align_inputs ¶
align_inputs(
names: object,
spec: LayeredSpec | AdjacencySpec,
) -> np.ndarray
Return an integer index that orders features to spec inputs.
The columns of a feature table rarely come in the order of
spec.input_nodes, which the parsed edgelist fixes. The
model reads columns by position, not by name, so a column
in the wrong place feeds the wrong input node without any
error. This returns a 1-D int64 index into the caller's
feature axis; indexing that axis with it puts the columns in
input-node order. It does not take the matrix, copy sample
rows, or densify. Apply the index on whatever holds X:
X[:, col] for numpy, scipy CSR/CSC, AnnData .X in
those formats, or a tensor; df.to_numpy()[:, col] for
a DataFrame. COO-style sparse layouts do not support
integer column indexing; convert first. Input nodes always
have width 1, so the index has one position per input node.
Call it after parsing, once, instead of hand-ordering
columns. Pass the names, not the data: DataFrames, tensors,
and matrices are rejected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
sequence of labels
|
The feature names, one per column, in the order the
columns sit in your data: |
required |
|
LayeredSpec or AdjacencySpec
|
Parsed edgelist whose |
required |
Returns:
| Type | Description |
|---|---|
numpy.ndarray of dtype int64, shape (len(spec.input_nodes),)
|
Positions into |
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
See Also
parse_layered : Builds the LayeredSpec whose
input_nodes set this column order.
parse_adjacency : Builds the AdjacencySpec for the packed
layout, where the result needs scattering first.
gather_hop_inputs : Assembles a later hop's input; layer 0
comes from indexing with this result instead.
Notes
Call it for every feature table, including one whose columns
already follow spec.input_nodes: it is the one check
that the data match the graph's input nodes by name.
Input nodes are not declared; they are the nodes with no
incoming edge. The edgelist therefore starts at the measured
features, one input node per feature column, named like that
column. Name measured features so no computed node shares a
name, for example STAT3_gene for the gene and STAT3
for the transcription factor node. Then an edgelist that
starts at the first computed layer, which makes those
computed nodes inputs, raises here as missing names. One
name is one node, so a feature named like a hidden or output
node has no input node of its own, and that raises too. With
shared names, such as transcription factor nodes named by
gene symbol beside a table of every gene, an edgelist that
starts at a computed layer can match feature columns and
pass.
AnnData,
numpy matrices, and scipy sparse matrices are not accepted
as names; pass the labels that sit on that axis and
apply the index on the matrix. scipy CSR/CSC and AnnData
.X in those formats stay sparse under X[:, col]
until the caller densifies one row block. COO and similar
layouts cannot be indexed that way.
hops[0] of a LayeredSpec reads layer 0 alone, so a
row block indexed with this result feeds the first hop
directly and needs no gathering. For an AdjacencySpec
the returned length is not the state width: to_mask() is
(state_dim, state_dim) over every node's units, while
the index covers the input units only
(len(spec.input_index)). Scatter the gathered columns
into the spec.state_dim-wide state vector with
state.index_copy(-1, input_index, x) before calling
MaskedLinear(spec.to_mask()). input_index is a
buffer, torch.as_tensor(spec.input_index).
Examples:
Names that are not graph nodes are ignored, and the rest are reordered:
>>> import numpy as np
>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
... {
... "source": ["A", "H"],
... "target": ["H", "C"],
... }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> spec.input_nodes
('A',)
>>> names = ["unused", "A"]
>>> col = kpnn2.align_inputs(names, spec)
>>> col.dtype
dtype('int64')
>>> col.tolist()
[1]
>>> values = np.array([[9.0, 0.5], [8.0, 1.5]])
>>> values[:, col].tolist()
[[0.5], [1.5]]
An AdjacencySpec works the same way, but the index is
len(spec.input_index) long and the gathered columns must be
scattered into the state vector before they reach
MaskedLinear(spec.to_mask()):
>>> import torch
>>> cyclic = pd.DataFrame(
... {
... "source": ["x", "a", "b", "a"],
... "target": ["a", "b", "a", "y"],
... }
... )
>>> state_spec = kpnn2.parse_adjacency(cyclic)
>>> col = kpnn2.align_inputs(["x"], state_spec)
>>> col.tolist(), tuple(state_spec.to_mask().shape)
([0], (4, 4))
>>> x = torch.tensor([[0.5], [1.5]])[:, col]
>>> state = torch.zeros(
... 2,
... state_spec.state_dim,
... )
>>> index = torch.as_tensor(state_spec.input_index)
>>> state = state.index_copy(
... -1,
... index,
... x,
... )
>>> state.tolist()
[[0.0, 0.0, 0.5, 0.0], [0.0, 0.0, 1.5, 0.0]]
A feature named like a node the model computes raises, because its column would never be read:
>>> computed = ["A", "H"]
>>> kpnn2.align_inputs(computed, spec)
Traceback (most recent call last):
...
Kpnn2Error: Feature names match node(s) the model computes, ...
A tensor is not accepted; pass the names that label it:
>>> t = torch.tensor([[0.5], [1.5]])
>>> kpnn2.align_inputs(t, spec)
Traceback (most recent call last):
...
Kpnn2Error: 'names' is a tensor; pass the feature names that ...