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 |
|---|---|---|---|
|
tuple[str, ...]
|
In-degree 0 names, alphabetical. This is the feature-axis
order |
required |
|
tuple[str, ...]
|
Out-degree 0 names, alphabetical. A terminal node below
maximum depth belongs here too, so this is not the same
tuple as |
required |
|
tuple[str, ...]
|
Names that are neither input nor output, alphabetical. |
required |
|
tuple[tuple[str, ...], ...]
|
|
required |
|
tuple[int, ...]
|
Unit count of each layer: |
required |
|
tuple[tuple[int, ...], ...]
|
|
required |
|
tuple[Hop, ...]
|
One hop per layer after the first:
|
required |
|
tuple[Skip, ...]
|
Original edges with depth gap greater than 1, as metadata.
Each one is already a block of packed unit pairs in
|
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 |
|---|---|---|---|
|
str
|
Source node name. Non-strings are converted with
|
required |
|
str
|
Target node name. Non-strings are converted with
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
hop_index |
int
|
Index |
packed_indices |
tuple of int
|
Indices into |
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
|
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 |
|---|---|---|---|
|
str
|
Node name. Non-strings are converted with
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
layer |
int
|
Depth of the node: index into |
units |
slice
|
Contiguous columns on that layer's last axis. Length is the node's width. |
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
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
|
A hop from |
required |
|
str
|
Node name. Non-strings are converted with
|
required |
Returns:
| Type | Description |
|---|---|
slice
|
Contiguous columns on |
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
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 |
|---|---|---|---|
|
dict
|
A dict with |
required |
Returns:
| Type | Description |
|---|---|
LayeredSpec
|
The parsed spec. |
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
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