26. Skip to content

26. Inductive API

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

26.1 What it is for

The inductive brick defines method registries and datasets for SSL methods that do not require a graph. [1][2]

26.2 Examples

Instantiate a method by ID:

import numpy as np
from modssc.inductive import DeviceSpec, InductiveDataset, get_method_class
from modssc.inductive.methods.pseudo_label import PseudoLabelSpec

X_l = np.random.randn(5, 4)
y_l = np.array([0, 1, 0, 1, 0])
X_u = np.random.randn(20, 4)

spec = PseudoLabelSpec(classifier_id="knn", classifier_backend="numpy")
method = get_method_class("pseudo_label")(spec=spec)
method.fit(InductiveDataset(X_l=X_l, y_l=y_l, X_u=X_u), device=DeviceSpec(device="cpu"), seed=0)

List available method IDs:

from modssc.inductive import available_methods

print(available_methods())

The registry and dataset types are in src/modssc/inductive/registry.py and src/modssc/inductive/types.py. [1][2]

26.3 API reference

Inductive semi-supervised learning (planned).

This package defines the interfaces and registry for inductive methods. The inductive brick is read-only with respect to input artifacts; any data modifications must be handled by upstream bricks.

26.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/inductive/types.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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"

26.5 InductiveDataset dataclass

Read-only input bundle for inductive SSL methods.

The inductive brick must not mutate these arrays; any data changes should be handled by upstream bricks (sampling, preprocess, augmentation, views).

Source code in src/modssc/inductive/types.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@dataclass(frozen=True)
class InductiveDataset:
    """Read-only input bundle for inductive SSL methods.

    The inductive brick must not mutate these arrays; any data changes should
    be handled by upstream bricks (sampling, preprocess, augmentation, views).
    """

    X_l: Any
    y_l: Any
    X_u: Any | None = None
    X_u_w: Any | None = None
    X_u_s: Any | None = None
    views: Mapping[str, Any] | None = None
    meta: Mapping[str, Any] | None = None

26.6 InductiveMethod

Bases: Protocol

Common interface for inductive methods.

Source code in src/modssc/inductive/base.py
41
42
43
44
45
46
47
48
49
50
class InductiveMethod(Protocol):
    """Common interface for inductive methods."""

    info: MethodInfo

    def fit(
        self, data: InductiveDatasetLike, *, device: DeviceSpec, seed: int = 0
    ) -> InductiveMethod: ...

    def predict_proba(self, X: Any) -> Any: ...

26.7 InductiveNotImplementedError

Bases: NotImplementedError

Raised when a method is registered but not implemented yet.

Source code in src/modssc/inductive/errors.py
23
24
25
26
27
28
29
30
31
32
class InductiveNotImplementedError(NotImplementedError):
    """Raised when a method is registered but not implemented yet."""

    def __init__(self, method_id: str, hint: str | None = None) -> None:
        msg = f"Inductive method {method_id!r} is registered but not implemented yet."
        if hint:
            msg = f"{msg} {hint}"
        super().__init__(msg)
        self.method_id = method_id
        self.hint = hint

26.8 InductiveValidationError

Bases: ValueError

Raised when inputs are invalid for inductive methods.

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

26.9 MethodInfo dataclass

Metadata for an inductive method.

Source code in src/modssc/inductive/base.py
26
27
28
29
30
31
32
33
34
35
36
37
38
@dataclass(frozen=True)
class MethodInfo:
    """Metadata for an inductive method."""

    method_id: str
    name: str
    year: int | None = None
    family: str | None = None  # pseudo-label, consistency, mixup, teacher, agreement
    supports_gpu: bool = True
    required_extra: str | None = None
    paper_title: str | None = None
    paper_pdf: str | None = None
    official_code: str | None = None

26.10 NumpyDataset dataclass

Strict numpy view of an inductive dataset (no implicit conversion).

Source code in src/modssc/inductive/adapters/numpy.py
42
43
44
45
46
47
48
49
50
51
52
@dataclass(frozen=True)
class NumpyDataset:
    """Strict numpy view of an inductive dataset (no implicit conversion)."""

    X_l: np.ndarray
    y_l: np.ndarray
    X_u: np.ndarray | None = None
    X_u_w: np.ndarray | None = None
    X_u_s: np.ndarray | None = None
    views: Mapping[str, np.ndarray] | None = None
    meta: Mapping[str, Any] | None = None

26.11 OptionalDependencyError dataclass

Bases: ImportError

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

Source code in src/modssc/inductive/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}]"'

26.12 TorchDataset dataclass

Strict torch view of an inductive dataset (no implicit conversion).

Source code in src/modssc/inductive/adapters/torch.py
79
80
81
82
83
84
85
86
87
88
89
@dataclass(frozen=True)
class TorchDataset:
    """Strict torch view of an inductive dataset (no implicit conversion)."""

    X_l: Any
    y_l: Any
    X_u: Any | None = None
    X_u_w: Any | None = None
    X_u_s: Any | None = None
    views: Mapping[str, Any] | None = None
    meta: Mapping[str, Any] | None = None

26.13 TorchModelBundle dataclass

Torch model bundle provided by the deep-model brick.

Source code in src/modssc/inductive/deep/types.py
 8
 9
10
11
12
13
14
15
16
17
@dataclass(frozen=True)
class TorchModelBundle:
    """Torch model bundle provided by the deep-model brick."""

    model: Any
    optimizer: Any
    ema_model: Any | None = None
    scheduler: Any | None = None
    scaler: Any | None = None
    meta: Mapping[str, Any] | None = None

26.14 make_numpy_rng(seed)

Create a numpy Generator seeded with the given seed.

Source code in src/modssc/inductive/seed.py
12
13
14
def make_numpy_rng(seed: int) -> np.random.Generator:
    """Create a numpy Generator seeded with the given seed."""
    return np.random.default_rng(int(seed))

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

Register a method by id and a lazy import string.

Source code in src/modssc/inductive/registry.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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)

26.16 seed_everything(seed, *, deterministic=True)

Best-effort deterministic seeding across random, numpy, and torch (if available).

Source code in src/modssc/inductive/seed.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def seed_everything(seed: int, *, deterministic: bool = True) -> None:
    """Best-effort deterministic seeding across random, numpy, and torch (if available)."""
    seed_i = int(seed)
    random.seed(seed_i)
    np.random.seed(seed_i)

    try:
        torch = optional_import("torch", extra="inductive-torch")
    except OptionalDependencyError:
        return

    torch.manual_seed(seed_i)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed_i)

    if deterministic:
        if hasattr(torch, "use_deterministic_algorithms"):
            with suppress(Exception):
                torch.use_deterministic_algorithms(True)
        if hasattr(torch.backends, "cudnn"):
            torch.backends.cudnn.deterministic = True
            torch.backends.cudnn.benchmark = False

26.17 to_numpy_dataset(data)

Validate and wrap an inductive dataset backed by numpy arrays.

Source code in src/modssc/inductive/adapters/numpy.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def to_numpy_dataset(data: InductiveDatasetLike) -> NumpyDataset:
    """Validate and wrap an inductive dataset backed by numpy arrays."""
    validate_inductive_dataset(data)

    X_l = _require_numpy(data.X_l, name="X_l")
    y_l = _require_numpy(data.y_l, name="y_l")
    X_u = _require_numpy(data.X_u, name="X_u") if data.X_u is not None else None
    X_u_w = _require_numpy(data.X_u_w, name="X_u_w") if data.X_u_w is not None else None
    X_u_s = _require_numpy(data.X_u_s, name="X_u_s") if data.X_u_s is not None else None
    views = _require_numpy_views(data.views)

    return NumpyDataset(
        X_l=X_l,
        y_l=y_l,
        X_u=X_u,
        X_u_w=X_u_w,
        X_u_s=X_u_s,
        views=views,
        meta=data.meta,
    )

26.18 to_torch_dataset(data, *, device=None, require_same_device=True)

Validate and wrap an inductive dataset backed by torch tensors.

If device.device == "auto", only device consistency is enforced.

Source code in src/modssc/inductive/adapters/torch.py
 92
 93
 94
 95
 96
 97
 98
 99
100
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def to_torch_dataset(
    data: InductiveDatasetLike,
    *,
    device: DeviceSpec | None = None,
    require_same_device: bool = True,
) -> TorchDataset:
    """Validate and wrap an inductive dataset backed by torch tensors.

    If device.device == "auto", only device consistency is enforced.
    """
    if data is None:
        raise InductiveValidationError("data must not be None.")
    X_l = _require_tensor(data.X_l, name="X_l")
    y_l = _require_tensor(data.y_l, name="y_l")
    _check_2d(X_l, name="X_l")
    _check_y(y_l, n=int(X_l.shape[0]))

    X_u = _require_tensor(data.X_u, name="X_u") if data.X_u is not None else None
    X_u_w = _require_tensor(data.X_u_w, name="X_u_w") if data.X_u_w is not None else None
    X_u_s = _require_tensor(data.X_u_s, name="X_u_s") if data.X_u_s is not None else None

    n_features = int(X_l.shape[1])
    if X_u is not None:
        _check_2d(X_u, name="X_u")
        _check_feature_dim(X_u, n_features=n_features, name="X_u")
    if X_u_w is not None:
        _check_2d(X_u_w, name="X_u_w")
        _check_feature_dim(X_u_w, n_features=n_features, name="X_u_w")
    if X_u_s is not None:
        _check_2d(X_u_s, name="X_u_s")
        _check_feature_dim(X_u_s, n_features=n_features, name="X_u_s")
    if X_u_w is not None and X_u_s is not None and int(X_u_w.shape[0]) != int(X_u_s.shape[0]):
        raise InductiveValidationError("X_u_w and X_u_s must have the same number of rows")

    views = _require_views(data.views)

    tensors = [X_l, y_l, X_u, X_u_w, X_u_s]
    if views:
        tensors.extend(views.values())

    if require_same_device:
        _check_same_device(tensors)

    if device is not None and device.device != "auto":
        expected = torch_backend.resolve_device(device)
        for t in tensors:
            if t is None:
                continue
            if t.device != expected:
                raise InductiveValidationError(
                    f"Tensor device mismatch: expected {expected}, got {t.device}"
                )

    return TorchDataset(
        X_l=X_l,
        y_l=y_l,
        X_u=X_u,
        X_u_w=X_u_w,
        X_u_s=X_u_s,
        views=views,
        meta=data.meta,
    )

26.19 validate_inductive_dataset(data)

Validate the minimal invariants needed by inductive algorithms.

Source code in src/modssc/inductive/validation.py
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
def validate_inductive_dataset(data: InductiveDatasetLike) -> None:
    """Validate the minimal invariants needed by inductive algorithms."""
    if data is None:
        raise InductiveValidationError("data must not be None")

    X_l = _require_2d(data.X_l, name="X_l")
    _require_y(data.y_l, n=int(X_l.shape[0]))

    n_features = int(X_l.shape[1])

    if data.X_u is not None:
        X_u = _require_2d(data.X_u, name="X_u")
        if int(X_u.shape[1]) != n_features:
            raise InductiveValidationError("X_u must have the same feature dimension as X_l")

    if data.X_u_w is not None:
        X_u_w = _require_2d(data.X_u_w, name="X_u_w")
        if int(X_u_w.shape[1]) != n_features:
            raise InductiveValidationError("X_u_w must have the same feature dimension as X_l")

    if data.X_u_s is not None:
        X_u_s = _require_2d(data.X_u_s, name="X_u_s")
        if int(X_u_s.shape[1]) != n_features:
            raise InductiveValidationError("X_u_s must have the same feature dimension as X_l")

    if data.X_u_w is not None and data.X_u_s is not None:
        X_u_w = _as_numpy(data.X_u_w)
        X_u_s = _as_numpy(data.X_u_s)
        if X_u_w.shape[0] != X_u_s.shape[0]:
            raise InductiveValidationError("X_u_w and X_u_s must have the same number of rows")

    _validate_views(data.views)
    _validate_meta(data.meta)

26.20 validate_torch_model_bundle(bundle)

Validate a torch model bundle (model + optimizer).

Source code in src/modssc/inductive/deep/validation.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def validate_torch_model_bundle(bundle: TorchModelBundle) -> TorchModelBundle:
    """Validate a torch model bundle (model + optimizer)."""
    torch = optional_import("torch", extra="inductive-torch")
    if not isinstance(bundle, TorchModelBundle):
        raise InductiveValidationError("model_bundle must be a TorchModelBundle.")
    if not isinstance(bundle.model, torch.nn.Module):
        raise InductiveValidationError("model_bundle.model must be a torch.nn.Module.")
    if not isinstance(bundle.optimizer, torch.optim.Optimizer):
        raise InductiveValidationError("model_bundle.optimizer must be a torch.optim.Optimizer.")

    params = [p for p in bundle.model.parameters() if p.requires_grad]
    if not params:
        raise InductiveValidationError("model_bundle.model must have trainable parameters.")

    model_ids = {id(p) for p in bundle.model.parameters()}
    for group in bundle.optimizer.param_groups:
        for p in group.get("params", []):
            if id(p) not in model_ids:
                raise InductiveValidationError(
                    "model_bundle.optimizer params must come from model parameters."
                )

    if bundle.ema_model is not None and not isinstance(bundle.ema_model, torch.nn.Module):
        raise InductiveValidationError("model_bundle.ema_model must be a torch.nn.Module.")

    return bundle
Sources
  1. src/modssc/inductive/registry.py
  2. src/modssc/inductive/types.py