Skip to content

Source centroid decoder

neureptrace.decoding.source_centroid implements a dependency-light strict source-only nearest-centroid decoder.

The protocol is Category 1 / strict source-only. Class centroids are estimated from source features and source labels only; held-out rows are scored by scaled squared distance to those source centroids.

Optional centroid shrinkage moves source class centroids toward the source global mean. This remains source-only because no held-out target rows or labels are used for fitting.

neureptrace.decoding.source_centroid

Source-only nearest-centroid decoder.

This module provides a small dependency-light baseline for strict cross-subject feature decoding. Class centroids are estimated from source rows and source labels only; held-out rows are scored by scaled squared distance to the source centroids.

SourceCentroidConfig dataclass

Configuration for the source-centroid decoder.

Source code in src/neureptrace/decoding/source_centroid.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@dataclass(frozen=True, slots=True)
class SourceCentroidConfig:
    """Configuration for the source-centroid decoder."""

    temperature: float = DEFAULT_TEMPERATURE
    use_diagonal_scale: bool = True
    shrinkage: float = 0.0
    epsilon: float = DEFAULT_EPSILON

    def __post_init__(self) -> None:
        """Normalize and validate direct dataclass construction."""

        object.__setattr__(self, "temperature", _positive_float(self.temperature, name="temperature"))
        object.__setattr__(self, "use_diagonal_scale", _boolean(self.use_diagonal_scale, name="use_diagonal_scale"))
        object.__setattr__(self, "shrinkage", _unit_interval_float(self.shrinkage, name="shrinkage"))
        object.__setattr__(self, "epsilon", _positive_float(self.epsilon, name="epsilon"))

__post_init__()

Normalize and validate direct dataclass construction.

Source code in src/neureptrace/decoding/source_centroid.py
33
34
35
36
37
38
39
def __post_init__(self) -> None:
    """Normalize and validate direct dataclass construction."""

    object.__setattr__(self, "temperature", _positive_float(self.temperature, name="temperature"))
    object.__setattr__(self, "use_diagonal_scale", _boolean(self.use_diagonal_scale, name="use_diagonal_scale"))
    object.__setattr__(self, "shrinkage", _unit_interval_float(self.shrinkage, name="shrinkage"))
    object.__setattr__(self, "epsilon", _positive_float(self.epsilon, name="epsilon"))

SourceCentroidResult dataclass

Nearest-centroid predictions and provenance metadata.

Source code in src/neureptrace/decoding/source_centroid.py
42
43
44
45
46
47
48
49
50
51
52
@dataclass(frozen=True, slots=True)
class SourceCentroidResult:
    """Nearest-centroid predictions and provenance metadata."""

    probabilities: np.ndarray
    predictions: np.ndarray
    classes: np.ndarray
    centroids: np.ndarray
    feature_scale: np.ndarray
    distances: np.ndarray
    metadata: dict[str, Any] = field(default_factory=dict)

fit_source_centroid_decoder(*, source_features, source_labels, test_features, config=None)

Fit source class centroids and score test rows.

This is a strict source-only baseline. The API has no target adaptation inputs and no target-label inputs.

Source code in src/neureptrace/decoding/source_centroid.py
 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
101
102
103
104
def fit_source_centroid_decoder(
    *,
    source_features: Sequence[Sequence[float]] | np.ndarray,
    source_labels: Sequence[Any] | np.ndarray,
    test_features: Sequence[Sequence[float]] | np.ndarray,
    config: SourceCentroidConfig | Mapping[str, Any] | None = None,
) -> SourceCentroidResult:
    """Fit source class centroids and score test rows.

    This is a strict source-only baseline.  The API has no target adaptation
    inputs and no target-label inputs.
    """

    cfg = source_centroid_config() if config is None else _coerce_config(config)
    source = _feature_matrix(source_features, name="source_features")
    test = _feature_matrix(test_features, name="test_features")
    if source.shape[1] != test.shape[1]:
        raise ValueError(f"source_features and test_features must have the same feature width: {source.shape[1]} != {test.shape[1]}.")
    labels = _label_vector(source_labels, expected_length=source.shape[0], name="source_labels")
    classes = _unique_labels(labels)
    if classes.shape[0] < 2:
        raise ValueError("At least two source classes are required.")

    centroids, counts = _class_centroids(source, labels, classes=classes)
    if cfg.shrinkage > 0.0:
        global_mean = np.mean(source, axis=0, keepdims=True)
        centroids = (1.0 - cfg.shrinkage) * centroids + cfg.shrinkage * global_mean
    scale = _feature_scale(source, enabled=cfg.use_diagonal_scale, epsilon=cfg.epsilon)
    distances = _scaled_squared_distances(test, centroids, scale=scale)
    probabilities = _softmax(-distances / cfg.temperature)
    predictions = classes[np.argmax(probabilities, axis=1)]
    metadata = _metadata(
        cfg,
        n_source_rows=source.shape[0],
        n_test_rows=test.shape[0],
        feature_dim=source.shape[1],
        classes=classes,
        counts=counts,
    )
    return SourceCentroidResult(
        probabilities=probabilities.astype(np.float32, copy=False),
        predictions=predictions,
        classes=classes,
        centroids=centroids.astype(np.float32, copy=False),
        feature_scale=scale.astype(np.float32, copy=False),
        distances=distances.astype(np.float32, copy=False),
        metadata=metadata,
    )

source_centroid_config(*, temperature=DEFAULT_TEMPERATURE, use_diagonal_scale=True, shrinkage=0.0, epsilon=DEFAULT_EPSILON)

Normalize public source-centroid options.

Source code in src/neureptrace/decoding/source_centroid.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def source_centroid_config(
    *,
    temperature: float | str = DEFAULT_TEMPERATURE,
    use_diagonal_scale: bool | str | int | float = True,
    shrinkage: float | str = 0.0,
    epsilon: float | str = DEFAULT_EPSILON,
) -> SourceCentroidConfig:
    """Normalize public source-centroid options."""

    return SourceCentroidConfig(
        temperature=_positive_float(temperature, name="temperature"),
        use_diagonal_scale=_boolean(use_diagonal_scale, name="use_diagonal_scale"),
        shrinkage=_unit_interval_float(shrinkage, name="shrinkage"),
        epsilon=_positive_float(epsilon, name="epsilon"),
    )