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
| 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
| 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
src/modssc/inductive/registry.py
src/modssc/inductive/types.py