Utils¶
graph ¶
Graph utilities wrapping optional NetworkX and RDFLib dependencies, plus
pykeen-TriplesFactory-compatible id mapping and train/test splitting
that work without pykeen installed.
build_id_mappings ¶
build_id_mappings(
triples: list[Triple],
) -> tuple[dict[str, int], dict[str, int]]
Build pykeen-TriplesFactory-compatible entity/relation id mappings.
Mirrors what pykeen.triples.TriplesFactory.from_labeled_triples builds
internally when no explicit mapping is given: entities are the sorted
union of all subjects and objects, relations are the sorted set of
predicates, each enumerated from 0. Sorting makes the mapping
deterministic regardless of triple order/duplication.
Build this from the complete triple set (e.g. before splitting via train_test_split_triples) so that a train split and a test split can share one consistent id space -- see that function's docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
list[Triple]
|
Triples to derive ids from, e.g. from to_triples. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
dict[str, int]
|
is empty. |
Source code in src/linkingtk/utils/graph.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
map_triples_to_ids ¶
map_triples_to_ids(
triples: list[Triple],
entity_to_id: Mapping[str, int],
relation_to_id: Mapping[str, int],
) -> npt.NDArray[np.int64]
Map label triples to a pykeen-TriplesFactory-ready id array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
list[Triple]
|
Triples to map. |
required |
entity_to_id
|
Mapping[str, int]
|
Subject/object label -> id, e.g. from build_id_mappings. |
required |
relation_to_id
|
Mapping[str, int]
|
Predicate label -> id. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
An |
NDArray[int64]
|
rows, in the same column order pykeen's |
NDArray[int64]
|
uses -- pass it straight through as that constructor's |
NDArray[int64]
|
|
NDArray[int64]
|
empty. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a triple references a label absent from
|
Source code in src/linkingtk/utils/graph.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
to_triples ¶
to_triples(
graph: Graph, *, relation_attr: str = "relation"
) -> list[Triple]
Normalize a supported graph representation into a plain triple list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
graph
|
Graph
|
A list of |
required |
relation_attr
|
str
|
For |
'relation'
|
Returns:
| Type | Description |
|---|---|
list[Triple]
|
The graph as a list of triples. Empty if |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/linkingtk/utils/graph.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
train_test_split_triples ¶
train_test_split_triples(
triples: list[Triple],
test_size: float = 0.2,
random_state: int | None = None,
) -> tuple[list[Triple], list[Triple]]
Randomly split triples into train/test lists.
A thin wrapper around sklearn.model_selection.train_test_split (no
pykeen dependency, so this works without the kge extra installed).
Unlike pykeen.triples.TriplesFactory.split, this does not
guarantee every entity/relation appears in the train split -- it's a
plain uniform-random row split over triples, so an entity that
happens to have all its triples land in the test partition will be
absent from train (and vice versa). Callers that need that coverage
guarantee should build a TriplesFactory and call its own
.split() instead.
Build id mappings (via
build_id_mappings) from the
full triples list passed here, not from the returned splits
independently, so both splits share one consistent id space -- e.g.::
train, test = train_test_split_triples(triples, test_size=0.2, random_state=0)
entity_to_id, relation_to_id = build_id_mappings(triples)
train_ids = map_triples_to_ids(train, entity_to_id, relation_to_id)
test_ids = map_triples_to_ids(test, entity_to_id, relation_to_id)
Self-loops (subject == object) and multi-edges/duplicate triples
(e.g. from a networkx.MultiDiGraph via
to_triples) are treated as
ordinary, independent rows -- no deduplication or entity/relation-aware
grouping is performed, so two triples that share an entity pair can
land on opposite sides of the split.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
list[Triple]
|
Triples to split. |
required |
test_size
|
float
|
Fraction of |
0.2
|
random_state
|
int | None
|
Seed for reproducible splitting; |
None
|
Returns:
| Type | Description |
|---|---|
list[Triple]
|
|
list[Triple]
|
empty. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Propagated from sklearn if |
Source code in src/linkingtk/utils/graph.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
device ¶
Shared device-resolution helper for PyTorch-backed training code.
KGELinker is excluded -- it's
pykeen-backed, and pykeen's own trainer already handles device placement.
Every other linker in linkingtk.algorithms.ea (MTransELinker,
IPTransELinker, JAPELinker, KDCoELinker, AttrELinker,
IMUSELinker, MultiKELinker) hand-rolls its own PyTorch training loop
and calls resolve_device once at
the top of fit() -- as does
Trainer.
resolve_device ¶
resolve_device(device: str) -> torch.device
Resolve a device string (e.g. "cpu", "cuda", "cuda:0") to a torch.device.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
device
|
str
|
A device string acceptable to |
required |
Returns:
| Type | Description |
|---|---|
device
|
The resolved |
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
Source code in src/linkingtk/utils/device.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
sparse_gcn ¶
Shared sparse-adjacency helpers for the GNN-based EA linkers (GCNAlignLinker, RDGCNLinker, AliNetLinker).
Every one of the three needs the same symmetric-degree-normalize step over a
(weighted or unweighted) adjacency after self-loops are added --
normalize_adjacency_coo below is that one piece of math, ported once. The
edge weighting itself (GCN-Align's relation-functionality weighting,
RDGCN's plain unweighted primal graph, AliNet's plain unweighted 1-/2-hop
graphs) is genuinely method-specific and stays in each linker's own private
_<name>_training.py.
Kept as its own module (not folded into
linkingtk.utils.graph) so that module can stay
torch-free -- coo_to_torch_sparse imports torch lazily inside the
function body, matching every other optional-torch import in this package
(see resolve_device).
coo_to_torch_sparse ¶
coo_to_torch_sparse(
indices: NDArray[int64],
values: NDArray[float64],
size: tuple[int, int],
device: device,
) -> torch.Tensor
Build a coalesced torch.sparse_coo_tensor from COO arrays.
Thin wrapper around the same torch.sparse_coo_tensor(...).coalesce()
pattern already used in
StructuredLogits --
reused here instead of each GNN linker's own _<name>_torch.py
reimplementing it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
NDArray[int64]
|
|
required |
values
|
NDArray[float64]
|
|
required |
size
|
tuple[int, int]
|
|
required |
device
|
device
|
Target device. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
A coalesced sparse COO tensor on |
Source code in src/linkingtk/utils/sparse_gcn.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | |
normalize_adjacency_coo ¶
normalize_adjacency_coo(
indices: NDArray[int64],
values: NDArray[float64],
num_nodes: int,
add_self_loops: bool = True,
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]
Symmetrically degree-normalize a weighted adjacency: D^-0.5 A D^-0.5.
Degree D comes from A's column sums, not row sums -- this
only matters when A is asymmetric (true for GCN-Align's
relation-functionality-weighted edges; a no-op for RDGCN's/AliNet's
plain unweighted, inherently-symmetric adjacencies, where row- and
column-sums coincide). This is a deliberately-derived match for OpenEA's
own GCN_Utils.normalize_adj, not an arbitrary choice: that function
computes D^-0.5 A_stored^T D^-0.5 (row-sum degree, but with an
extra transpose) over a matrix A_stored that OpenEA's own
get_weighted_adj already builds row/col-swapped
(row.append(key[1]); col.append(key[0]) -- storing each edge at
[target, source], not [source, target]). Working through both
swaps algebraically: D^-0.5 A_stored^T D^-0.5 computed over that
swapped storage is exactly D^-0.5 A D^-0.5 computed over the
natural (source-row, target-column) orientation this function expects
(see
build_weighted_adjacency),
using column sums for the degree (since A_stored's row-sums are
A's column-sums once the swap unwinds). Confirmed empirically, not
just algebraically: on the real EN-FR-15K-V1 benchmark, this
column-sum version reaches Hits@10=0.243/MRR=0.154 vs. a row-sum
version's Hits@10=0.148/MRR=0.116 (300 epochs, otherwise identical) --
a meaningfully different, and clearly better, result, not an
inconsequential implementation detail. Duplicate (row, col) entries
in indices are summed before normalizing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
NDArray[int64]
|
|
required |
values
|
NDArray[float64]
|
|
required |
num_nodes
|
int
|
Total node count (both KGs combined, in this package's usage -- entity ids from build_id_mappings are already in one shared id space). |
required |
add_self_loops
|
bool
|
Whether to add weight- |
True
|
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[float64]
|
shape as the input, coalesced (no duplicate |
Source code in src/linkingtk/utils/sparse_gcn.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |