Skip to content

Subspace alignment

neureptrace.decoding.subspace_alignment implements PCA subspace alignment for cross-subject transfer.

The method fits a source PCA basis and an unlabeled target PCA basis, rotates the source coordinates toward the target coordinates, and then allows a source-label classifier to be trained in the aligned space.

Protocol boundary:

  • uses source features,
  • uses source labels only for the optional classifier helper,
  • uses unlabeled target features to fit the target subspace,
  • does not accept target labels.

neureptrace.decoding.subspace_alignment

PCA subspace alignment for unlabeled target-adaptive decoding.

This module implements a dependency-light version of feature-space subspace alignment for cross-subject transfer. A source PCA basis and an unlabeled target PCA basis are estimated inside one fold, the source basis is rotated toward the target basis, and the transformed source rows can be used with ordinary source-label classifiers. Target labels are intentionally absent from the public API.

SubspaceAlignmentModel dataclass

Fitted source-to-target PCA subspace alignment model.

Source code in src/neureptrace/decoding/subspace_alignment.py
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
@dataclass(frozen=True, slots=True)
class SubspaceAlignmentModel:
    """Fitted source-to-target PCA subspace alignment model."""

    source_mean: np.ndarray
    source_scale: np.ndarray
    target_mean: np.ndarray
    target_scale: np.ndarray
    source_basis: np.ndarray
    target_basis: np.ndarray
    alignment_matrix: np.ndarray
    standardization_scope: str

    def transform_source(self, features: Sequence[Sequence[float]] | np.ndarray) -> np.ndarray:
        """Project source-domain rows into the target subspace coordinates."""

        matrix = _feature_matrix(features, name="features")
        if matrix.shape[1] != self.source_basis.shape[0]:
            raise ValueError(f"features width {matrix.shape[1]} does not match fitted width {self.source_basis.shape[0]}.")
        prepared = (matrix - self.source_mean) / self.source_scale
        return (prepared @ self.source_basis @ self.alignment_matrix).astype(np.float32, copy=False)

    def transform_target(self, features: Sequence[Sequence[float]] | np.ndarray) -> np.ndarray:
        """Project target-domain rows into their own PCA subspace coordinates."""

        matrix = _feature_matrix(features, name="features")
        if matrix.shape[1] != self.target_basis.shape[0]:
            raise ValueError(f"features width {matrix.shape[1]} does not match fitted width {self.target_basis.shape[0]}.")
        prepared = (matrix - self.target_mean) / self.target_scale
        return (prepared @ self.target_basis).astype(np.float32, copy=False)

transform_source(features)

Project source-domain rows into the target subspace coordinates.

Source code in src/neureptrace/decoding/subspace_alignment.py
43
44
45
46
47
48
49
50
def transform_source(self, features: Sequence[Sequence[float]] | np.ndarray) -> np.ndarray:
    """Project source-domain rows into the target subspace coordinates."""

    matrix = _feature_matrix(features, name="features")
    if matrix.shape[1] != self.source_basis.shape[0]:
        raise ValueError(f"features width {matrix.shape[1]} does not match fitted width {self.source_basis.shape[0]}.")
    prepared = (matrix - self.source_mean) / self.source_scale
    return (prepared @ self.source_basis @ self.alignment_matrix).astype(np.float32, copy=False)

transform_target(features)

Project target-domain rows into their own PCA subspace coordinates.

Source code in src/neureptrace/decoding/subspace_alignment.py
52
53
54
55
56
57
58
59
def transform_target(self, features: Sequence[Sequence[float]] | np.ndarray) -> np.ndarray:
    """Project target-domain rows into their own PCA subspace coordinates."""

    matrix = _feature_matrix(features, name="features")
    if matrix.shape[1] != self.target_basis.shape[0]:
        raise ValueError(f"features width {matrix.shape[1]} does not match fitted width {self.target_basis.shape[0]}.")
    prepared = (matrix - self.target_mean) / self.target_scale
    return (prepared @ self.target_basis).astype(np.float32, copy=False)

SubspaceAlignmentResult dataclass

Aligned source and target features plus protocol metadata.

Source code in src/neureptrace/decoding/subspace_alignment.py
62
63
64
65
66
67
68
69
@dataclass(frozen=True, slots=True)
class SubspaceAlignmentResult:
    """Aligned source and target features plus protocol metadata."""

    source_features: np.ndarray
    target_features: np.ndarray
    model: SubspaceAlignmentModel
    metadata: dict[str, Any] = field(default_factory=dict)

SubspaceAlignedClassificationResult dataclass

Classifier outputs from a source-label probe in aligned subspace.

Source code in src/neureptrace/decoding/subspace_alignment.py
72
73
74
75
76
77
78
79
80
81
82
83
@dataclass(frozen=True, slots=True)
class SubspaceAlignedClassificationResult:
    """Classifier outputs from a source-label probe in aligned subspace."""

    source_features: np.ndarray
    target_features: np.ndarray
    predictions: np.ndarray
    probabilities: np.ndarray | None
    classes: np.ndarray
    classifier: BaseEstimator
    model: SubspaceAlignmentModel
    metadata: dict[str, Any] = field(default_factory=dict)

fit_subspace_alignment(source_features, target_features, *, n_components=DEFAULT_SUBSPACE_COMPONENTS, standardization_scope='source')

Fit Category-2 PCA subspace alignment from source and unlabeled target rows.

Source code in src/neureptrace/decoding/subspace_alignment.py
 86
 87
 88
 89
 90
 91
 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
def fit_subspace_alignment(
    source_features: Sequence[Sequence[float]] | np.ndarray,
    target_features: Sequence[Sequence[float]] | np.ndarray,
    *,
    n_components: int | str | float = DEFAULT_SUBSPACE_COMPONENTS,
    standardization_scope: str | None = "source",
) -> SubspaceAlignmentResult:
    """Fit Category-2 PCA subspace alignment from source and unlabeled target rows."""

    source = _feature_matrix(source_features, name="source_features")
    target = _feature_matrix(target_features, name="target_features")
    if source.shape[1] != target.shape[1]:
        raise ValueError(f"source_features and target_features must have the same width: {source.shape[1]} != {target.shape[1]}.")
    scope = normalize_standardization_scope(standardization_scope)
    components = _effective_components(n_components, source_rows=source.shape[0], target_rows=target.shape[0], feature_dim=source.shape[1])
    source_mean, source_scale, target_mean, target_scale = _standardization(source, target, scope=scope)
    prepared_source = (source - source_mean) / source_scale
    prepared_target = (target - target_mean) / target_scale
    source_basis, source_variance = _pca_basis(prepared_source, components)
    target_basis, target_variance = _pca_basis(prepared_target, components)
    alignment = source_basis.T @ target_basis
    model = SubspaceAlignmentModel(
        source_mean=source_mean,
        source_scale=source_scale,
        target_mean=target_mean,
        target_scale=target_scale,
        source_basis=source_basis,
        target_basis=target_basis,
        alignment_matrix=alignment,
        standardization_scope=scope,
    )
    aligned_source = model.transform_source(source)
    aligned_target = model.transform_target(target)
    metadata = _metadata(
        n_source_rows=source.shape[0],
        n_target_rows=target.shape[0],
        feature_dim=source.shape[1],
        n_components=components,
        requested_components=n_components,
        standardization_scope=scope,
        source_explained_variance=source_variance,
        target_explained_variance=target_variance,
    )
    return SubspaceAlignmentResult(source_features=aligned_source, target_features=aligned_target, model=model, metadata=metadata)

fit_subspace_aligned_classifier(*, source_features, source_labels, target_features, n_components=DEFAULT_SUBSPACE_COMPONENTS, standardization_scope='source', classifier=None, classifier_C=1.0, classifier_max_iter=1000, classifier_class_weight='balanced', sample_weight=None)

Train a source-label classifier after Category-2 subspace alignment.

Source labels are encoded to dense integers before fitting so tuple/list style composite labels remain one class value per source row. Predictions and class vectors are decoded back to the original label objects.

Source code in src/neureptrace/decoding/subspace_alignment.py
132
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
187
188
189
190
191
192
193
194
195
196
197
def fit_subspace_aligned_classifier(
    *,
    source_features: Sequence[Sequence[float]] | np.ndarray,
    source_labels: Sequence[Any] | np.ndarray,
    target_features: Sequence[Sequence[float]] | np.ndarray,
    n_components: int | str | float = DEFAULT_SUBSPACE_COMPONENTS,
    standardization_scope: str | None = "source",
    classifier: BaseEstimator | None = None,
    classifier_C: float = 1.0,
    classifier_max_iter: int = 1000,
    classifier_class_weight: str | Mapping[Any, float] | None = "balanced",
    sample_weight: Sequence[float] | np.ndarray | None = None,
) -> SubspaceAlignedClassificationResult:
    """Train a source-label classifier after Category-2 subspace alignment.

    Source labels are encoded to dense integers before fitting so tuple/list style
    composite labels remain one class value per source row.  Predictions and class
    vectors are decoded back to the original label objects.
    """

    labels = _object_vector(source_labels, name="source_labels")
    aligned = fit_subspace_alignment(
        source_features,
        target_features,
        n_components=n_components,
        standardization_scope=standardization_scope,
    )
    if labels.shape[0] != aligned.source_features.shape[0]:
        raise ValueError(f"source_labels must contain one value per source row: {labels.shape[0]} != {aligned.source_features.shape[0]}.")
    classes, encoded_labels = _encode_object_labels(labels)
    if classes.shape[0] < 2:
        raise ValueError("source_labels must contain at least two classes.")
    weights = None if sample_weight is None else np.asarray(sample_weight, dtype=float).reshape(-1)
    if weights is not None:
        if weights.shape[0] != labels.shape[0]:
            raise ValueError(f"sample_weight must contain one value per source row: {weights.shape[0]} != {labels.shape[0]}.")
        if not np.all(np.isfinite(weights)) or np.any(weights < 0.0):
            raise ValueError("sample_weight must contain finite non-negative values.")
    class_weight = _encode_class_weight(classifier_class_weight, classes)
    model = clone(classifier) if classifier is not None else LogisticRegression(
        C=_positive_float(classifier_C, name="classifier_C"),
        class_weight=class_weight,
        max_iter=_positive_int(classifier_max_iter, name="classifier_max_iter"),
        random_state=13,
    )
    fit_kwargs = {} if weights is None else {"sample_weight": weights}
    model.fit(aligned.source_features, encoded_labels, **fit_kwargs)
    encoded_predictions = np.asarray(model.predict(aligned.target_features), dtype=int).reshape(-1)
    predictions = _decode_object_labels(encoded_predictions, classes)
    probabilities = _probabilities_or_none(model, aligned.target_features)
    metadata = {
        **aligned.metadata,
        "subspace_alignment_classifier": type(model).__name__,
        "subspace_alignment_uses_source_labels": True,
        "subspace_alignment_uses_target_labels": False,
    }
    return SubspaceAlignedClassificationResult(
        source_features=aligned.source_features,
        target_features=aligned.target_features,
        predictions=predictions,
        probabilities=probabilities,
        classes=classes,
        classifier=model,
        model=aligned.model,
        metadata=metadata,
    )

normalize_standardization_scope(value)

Normalize standardization-scope aliases.

Source code in src/neureptrace/decoding/subspace_alignment.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def normalize_standardization_scope(value: str | None) -> str:
    """Normalize standardization-scope aliases."""

    normalized = "source" if value is None else str(value).strip().lower().replace("-", "_")
    normalized = {
        "train": "source",
        "source_only": "source",
        "source_stats": "source",
        "pooled": "source_target",
        "source_plus_target": "source_target",
        "source_and_target": "source_target",
        "target_adaptive": "source_target",
        "off": "none",
        "false": "none",
        "identity": "none",
    }.get(normalized, normalized)
    if normalized not in SUBSPACE_STANDARDIZATION_SCOPES:
        raise ValueError(f"Unknown standardization_scope {value!r}. Available scopes: {', '.join(SUBSPACE_STANDARDIZATION_SCOPES)}.")
    return normalized