27. Skip to content

27. Transductive API

This page documents the transductive API. For a full run, see the transductive tutorial.

27.1 What it is for

The transductive brick provides registries and utilities for graph-based SSL methods. [1][2]

27.2 Examples

Run label propagation on a small graph:

import numpy as np
from modssc.graph import GraphBuilderSpec, build_graph
from modssc.graph.artifacts import NodeDataset
from modssc.transductive import get_method_class
from modssc.transductive.methods.classic.label_propagation import LabelPropagationSpec

X = np.random.randn(10, 4).astype(np.float32)
G = build_graph(X, spec=GraphBuilderSpec(scheme="knn", metric="cosine", k=3), seed=0, cache=False)
train_mask = np.zeros((10,), dtype=bool)
train_mask[:2] = True

node_ds = NodeDataset(X=X, y=np.zeros((10,), dtype=np.int64), graph=G, masks={"train_mask": train_mask})
method = get_method_class("label_propagation")(spec=LabelPropagationSpec())
method.fit(node_ds)
proba = method.predict_proba(node_ds)
print(proba.shape)

List available method IDs:

from modssc.transductive import available_methods

print(available_methods())

The method registry is defined in src/modssc/transductive/registry.py. [1]

27.3 API reference

Transductive semi supervised learning.

This package provides the math and integration layer: - backend abstraction (numpy, torch) - graph operators (normalization, laplacian, spmm) - generic solvers (fixed point, conjugate gradient) - PyG adapter (optional) - strict input validation

Algorithms (Label Propagation, Poisson Learning, GNNs, etc.) are added in later waves.

27.4 DeviceSpec dataclass

Device and dtype settings.

device
  • cpu: always CPU
  • cuda: use CUDA (error if unavailable)
  • mps: use Apple MPS (error if unavailable)
  • auto: pick cuda if available, else mps, else cpu

dtype: numeric precision to use for math operators

Source code in src/modssc/transductive/types.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@dataclass(frozen=True)
class DeviceSpec:
    """Device and dtype settings.

    device:
      - cpu: always CPU
      - cuda: use CUDA (error if unavailable)
      - mps: use Apple MPS (error if unavailable)
      - auto: pick cuda if available, else mps, else cpu

    dtype: numeric precision to use for math operators
    """

    device: DeviceName = "cpu"
    dtype: DTypeName = "float32"

27.5 OptionalDependencyError dataclass

Bases: ImportError

Raised when an optional dependency (extra) is required but missing.

Source code in src/modssc/transductive/errors.py
 6
 7
 8
 9
10
11
12
13
14
15
16
@dataclass(frozen=True)
class OptionalDependencyError(ImportError):
    """Raised when an optional dependency (extra) is required but missing."""

    package: str
    extra: str
    message: str | None = None

    def __str__(self) -> str:
        base = self.message or f"Optional dependency {self.package!r} is required."
        return f'{base} Install with: pip install "modssc[{self.extra}]"'

27.6 TransductiveValidationError

Bases: ValueError

Raised when inputs are invalid for transductive methods.

Source code in src/modssc/transductive/errors.py
19
20
class TransductiveValidationError(ValueError):
    """Raised when inputs are invalid for transductive methods."""

27.7 get_method_info(method_id)

Return the :class:~modssc.transductive.base.MethodInfo for a method.

Source code in src/modssc/transductive/registry.py
133
134
135
136
137
138
139
def get_method_info(method_id: str) -> MethodInfo:
    """Return the :class:`~modssc.transductive.base.MethodInfo` for a method."""
    cls = get_method_class(method_id)
    info = getattr(cls, "info", None)
    if not isinstance(info, MethodInfo):
        raise TypeError(f"Method class {cls} must expose a class attribute `info: MethodInfo`")
    return info

27.8 register_method(method_id, import_path, *, status='implemented')

Register a method by id and a lazy import string.

Source code in src/modssc/transductive/registry.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def register_method(
    method_id: str,
    import_path: str,
    *,
    status: Literal["implemented", "planned"] = "implemented",
) -> None:
    """Register a method by id and a lazy import string."""
    if not method_id or not isinstance(method_id, str):
        raise ValueError("method_id must be a non-empty string")
    if ":" not in import_path:
        raise ValueError("import_path must be of the form 'pkg.module:ClassName'")
    existing = _REGISTRY.get(method_id)
    if existing is not None and existing.import_path != import_path:
        raise ValueError(
            f"method_id {method_id!r} already registered with import_path={existing.import_path!r}"
        )
    if status not in {"implemented", "planned"}:
        raise ValueError("status must be 'implemented' or 'planned'")
    _REGISTRY[method_id] = MethodRef(method_id=method_id, import_path=import_path, status=status)

27.9 validate_node_dataset(data)

Validate the minimal invariants needed by transductive algorithms.

Source code in src/modssc/transductive/validation.py
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def validate_node_dataset(data: NodeDatasetLike) -> None:
    """Validate the minimal invariants needed by transductive algorithms."""
    if data is None:
        raise TransductiveValidationError("data must not be None")

    X = _as_numpy(data.X)
    y = _as_numpy(data.y)

    if X.ndim != 2:
        raise TransductiveValidationError(f"X must be 2D (n, d), got shape {X.shape}")
    if y.ndim != 1:
        raise TransductiveValidationError(f"y must be 1D (n,), got shape {y.shape}")
    if X.shape[0] != y.shape[0]:
        raise TransductiveValidationError("X and y must have the same first dimension")

    y_arr = np.asarray(y)
    if np.issubdtype(y_arr.dtype, np.integer):
        y_int = y_arr.astype(np.int64, copy=False)
    elif np.issubdtype(y_arr.dtype, np.floating):
        if not np.isfinite(y_arr).all():
            raise TransductiveValidationError(
                "y must contain finite integer class ids; found non-finite values"
            )
        y_int = y_arr.astype(np.int64)
        if not np.all(y_arr == y_int):
            raise TransductiveValidationError(
                "y must contain integer class ids; run preprocess step 'labels.encode'"
            )
    else:
        raise TransductiveValidationError(
            "y must contain integer class ids; run preprocess step 'labels.encode'"
        )

    y_valid = y_int[y_int >= 0]
    if y_valid.size:
        classes = np.unique(y_valid)
        if not np.array_equal(classes, np.arange(int(classes.size))):
            raise TransductiveValidationError(
                "y must contain contiguous class ids starting at 0; run preprocess step 'labels.encode'"
            )

    if data.graph is None:
        raise TransductiveValidationError("data.graph must not be None")

    raw_edge_index = getattr(data.graph, "edge_index", None)
    if raw_edge_index is None:
        raise TransductiveValidationError("graph.edge_index is required")
    edge_index = _as_numpy(raw_edge_index)

    if edge_index.ndim != 2 or edge_index.shape[0] != 2:
        raise TransductiveValidationError(
            f"edge_index must have shape (2, E), got {edge_index.shape}"
        )

    n = int(X.shape[0])
    if edge_index.size > 0 and (edge_index.min() < 0 or edge_index.max() >= n):
        raise TransductiveValidationError("edge_index has out of range node indices")

    masks: Mapping[str, Any] = data.masks or {}
    for key in ("train_mask", "val_mask", "test_mask", "unlabeled_mask"):
        if key not in masks:
            continue
        m = _as_numpy(masks[key]).astype(bool)
        if m.shape != (n,):
            raise TransductiveValidationError(f"{key} must have shape (n,), got {m.shape}")
Sources
  1. src/modssc/transductive/registry.py
  2. src/modssc/transductive/base.py