Skip to content

Precomputed foundation features

neureptrace.decoding.precomputed_foundation supports a dependency-light frozen-feature workflow for external EEG/MEG foundation encoders.

Use this when BENDR, LaBraM, EEGPT, CBraMod, or a project-local encoder has already exported a row-aligned feature table. NeuRepTrace then aligns rows by id and trains a source-label probe without importing the upstream model package.

Supported table formats:

  • .npz with a feature matrix and optional row ids,
  • .npy feature matrix with sequential row ids,
  • .csv / .tsv with a row-id column and numeric feature columns.

The module does not accept held-out target labels in the probe API. The feature_fit_scope metadata records whether the external feature extractor was declared as strict source-only, unlabeled target-adaptive, target-calibrated, or oracle/target-included.

neureptrace.decoding.precomputed_foundation

Precomputed foundation-feature tables for frozen M/EEG probes.

This module covers the dependency-light foundation-model path: external encoders such as BENDR, LaBraM, EEGPT, CBraMod, or project-local models can export a table of trial features, and NeuRepTrace can align those rows to fold-local metadata and train ordinary source-label probes without importing the upstream model package.

The loader/probe API intentionally does not accept held-out target labels. The feature_fit_scope metadata records how the external features were produced so strict source-only, unlabeled target-adaptive, and calibrated feature extractors remain distinguishable in reports.

PrecomputedFoundationFeatureTable dataclass

Immutable feature table keyed by row id.

Source code in src/neureptrace/decoding/precomputed_foundation.py
 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
@dataclass(frozen=True, slots=True)
class PrecomputedFoundationFeatureTable:
    """Immutable feature table keyed by row id."""

    features: np.ndarray
    row_ids: tuple[Any, ...]
    feature_names: tuple[str, ...]
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        matrix = _feature_matrix(self.features, name="features")
        row_ids = _row_id_tuple(self.row_ids, expected_length=matrix.shape[0], name="row_ids")
        feature_names = tuple(self.feature_names)
        if matrix.shape[1] != len(feature_names):
            raise ValueError(f"features and feature_names must have the same number of columns: {matrix.shape[1]} != {len(feature_names)}.")
        _validate_unique_row_ids(row_ids)
        object.__setattr__(self, "features", matrix.astype(np.float32, copy=False))
        object.__setattr__(self, "row_ids", row_ids)
        object.__setattr__(self, "feature_names", feature_names)

    @property
    def n_rows(self) -> int:
        """Number of rows in the feature table."""

        return int(self.features.shape[0])

    @property
    def n_features(self) -> int:
        """Number of feature columns in the table."""

        return int(self.features.shape[1])

    def row_index(self) -> dict[Any, int]:
        """Return a row-id to row-index mapping."""

        return {row_id: index for index, row_id in enumerate(self.row_ids)}

n_features property

Number of feature columns in the table.

n_rows property

Number of rows in the feature table.

row_index()

Return a row-id to row-index mapping.

Source code in src/neureptrace/decoding/precomputed_foundation.py
101
102
103
104
def row_index(self) -> dict[Any, int]:
    """Return a row-id to row-index mapping."""

    return {row_id: index for index, row_id in enumerate(self.row_ids)}

PrecomputedFoundationProbeResult dataclass

Source-label probe fitted on precomputed foundation features.

Source code in src/neureptrace/decoding/precomputed_foundation.py
107
108
109
110
111
112
113
114
115
116
117
@dataclass(frozen=True, slots=True)
class PrecomputedFoundationProbeResult:
    """Source-label probe fitted on precomputed foundation features."""

    train_features: np.ndarray
    test_features: np.ndarray
    predictions: np.ndarray
    probabilities: np.ndarray | None
    classes: np.ndarray
    classifier: BaseEstimator
    metadata: dict[str, Any] = field(default_factory=dict)

load_precomputed_foundation_features(path, *, features_key=DEFAULT_FEATURES_KEY, row_id_key=DEFAULT_ROW_ID_KEY, row_id_column=DEFAULT_ROW_ID_COLUMN, feature_columns=None, feature_prefix=None, allow_pickle=False, delimiter=None, feature_fit_scope='external_frozen', source_model='external')

Load precomputed foundation features from .npy, .npz, or CSV/TSV.

Source code in src/neureptrace/decoding/precomputed_foundation.py
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
154
155
156
157
158
159
160
161
162
163
164
def load_precomputed_foundation_features(
    path: str | Path,
    *,
    features_key: str = DEFAULT_FEATURES_KEY,
    row_id_key: str = DEFAULT_ROW_ID_KEY,
    row_id_column: str = DEFAULT_ROW_ID_COLUMN,
    feature_columns: Sequence[str] | str | None = None,
    feature_prefix: str | None = None,
    allow_pickle: bool = False,
    delimiter: str | None = None,
    feature_fit_scope: str | None = "external_frozen",
    source_model: str = "external",
) -> PrecomputedFoundationFeatureTable:
    """Load precomputed foundation features from ``.npy``, ``.npz``, or CSV/TSV."""

    table_path = Path(path)
    suffix = table_path.suffix.lower()
    scope = normalize_feature_fit_scope(feature_fit_scope)
    if suffix == ".npz":
        features, row_ids, feature_names = _load_npz_features(table_path, features_key=features_key, row_id_key=row_id_key, allow_pickle=allow_pickle)
    elif suffix == ".npy":
        features = np.load(table_path, allow_pickle=allow_pickle)
        features = _feature_matrix(features, name="features")
        row_ids = tuple(range(features.shape[0]))
        feature_names = tuple(f"foundation_{index}" for index in range(features.shape[1]))
    elif suffix in CSV_EXTENSIONS:
        features, row_ids, feature_names = _load_text_features(
            table_path,
            row_id_column=row_id_column,
            feature_columns=feature_columns,
            feature_prefix=feature_prefix,
            delimiter=delimiter,
        )
    else:
        raise ValueError(f"Unsupported feature table extension {suffix!r}; expected one of {sorted(NUMPY_EXTENSIONS | CSV_EXTENSIONS)}.")

    metadata = _feature_table_metadata(
        path=table_path,
        source_model=source_model,
        feature_fit_scope=scope,
        n_rows=features.shape[0],
        n_features=features.shape[1],
    )
    return PrecomputedFoundationFeatureTable(features=features, row_ids=tuple(row_ids), feature_names=tuple(feature_names), metadata=metadata)

make_precomputed_foundation_feature_table(features, row_ids=None, *, feature_names=None, feature_fit_scope='external_frozen', source_model='external')

Create a feature table directly from in-memory arrays.

Source code in src/neureptrace/decoding/precomputed_foundation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def make_precomputed_foundation_feature_table(
    features: Sequence[Sequence[float]] | np.ndarray,
    row_ids: Sequence[Any] | np.ndarray | None = None,
    *,
    feature_names: Sequence[str] | None = None,
    feature_fit_scope: str | None = "external_frozen",
    source_model: str = "external",
) -> PrecomputedFoundationFeatureTable:
    """Create a feature table directly from in-memory arrays."""

    matrix = _feature_matrix(features, name="features")
    ids = tuple(range(matrix.shape[0])) if row_ids is None else _row_id_tuple(row_ids, expected_length=matrix.shape[0], name="row_ids")
    names = tuple(f"foundation_{index}" for index in range(matrix.shape[1])) if feature_names is None else tuple(str(name) for name in feature_names)
    metadata = _feature_table_metadata(
        path=None,
        source_model=source_model,
        feature_fit_scope=normalize_feature_fit_scope(feature_fit_scope),
        n_rows=matrix.shape[0],
        n_features=matrix.shape[1],
    )
    return PrecomputedFoundationFeatureTable(features=matrix, row_ids=ids, feature_names=names, metadata=metadata)

align_precomputed_foundation_features(table, row_ids)

Return table rows in the requested row-id order.

Source code in src/neureptrace/decoding/precomputed_foundation.py
190
191
192
193
194
195
196
197
198
199
200
201
202
def align_precomputed_foundation_features(
    table: PrecomputedFoundationFeatureTable,
    row_ids: Sequence[Any] | np.ndarray,
) -> np.ndarray:
    """Return table rows in the requested row-id order."""

    index = table.row_index()
    requested = _requested_row_ids(row_ids, index)
    missing = [row_id for row_id in requested if row_id not in index]
    if missing:
        preview = ", ".join(repr(row_id) for row_id in missing[:5])
        raise KeyError(f"Precomputed feature table is missing {len(missing)} requested row id(s): {preview}.")
    return table.features[[index[row_id] for row_id in requested]].astype(np.float32, copy=False)

fit_precomputed_foundation_probe(*, feature_table, train_row_ids, train_labels, test_row_ids, classifier=None, classifier_C=1.0, classifier_max_iter=1000, classifier_class_weight='balanced', sample_weight=None)

Train a source-label probe on precomputed foundation features.

Source code in src/neureptrace/decoding/precomputed_foundation.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def fit_precomputed_foundation_probe(
    *,
    feature_table: PrecomputedFoundationFeatureTable,
    train_row_ids: Sequence[Any] | np.ndarray,
    train_labels: Sequence[Any] | np.ndarray,
    test_row_ids: Sequence[Any] | np.ndarray,
    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,
) -> PrecomputedFoundationProbeResult:
    """Train a source-label probe on precomputed foundation features."""

    labels = _label_vector(train_labels)
    train_ids = _row_id_tuple(train_row_ids, expected_length=labels.shape[0], name="train_row_ids")
    test_ids = _requested_row_ids(test_row_ids, feature_table.row_index())
    if labels.shape[0] != len(train_ids):
        raise ValueError(f"train_labels must contain one value per train row id: {labels.shape[0]} != {len(train_ids)}.")
    classes, fit_labels, decode_labels = _classifier_fit_labels(labels)
    if labels.shape[0] < 1 or classes.shape[0] < 2:
        raise ValueError("train_labels must contain at least two classes.")
    train_features = align_precomputed_foundation_features(feature_table, train_ids)
    test_features = align_precomputed_foundation_features(feature_table, test_ids)
    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 train 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.")

    model_class_weight = _encoded_class_weight(classifier_class_weight, classes) if decode_labels else classifier_class_weight
    model = clone(classifier) if classifier is not None else LogisticRegression(
        C=_positive_float(classifier_C, name="classifier_C"),
        max_iter=_positive_int(classifier_max_iter, name="classifier_max_iter"),
        class_weight=model_class_weight,
        random_state=13,
    )
    fit_kwargs = {} if weights is None else {"sample_weight": weights}
    model.fit(train_features, fit_labels, **fit_kwargs)
    classifier_model = classifiers.DecodedLabelClassifier(model, classes) if decode_labels else model
    predictions = np.asarray(classifier_model.predict(test_features))
    probabilities = _predict_probabilities_or_none(classifier_model, test_features)
    output_classes = np.asarray(getattr(classifier_model, "classes_", classes))
    metadata = _probe_metadata(feature_table, n_train_rows=len(train_ids), n_test_rows=len(test_ids), classifier=classifier_model)
    return PrecomputedFoundationProbeResult(
        train_features=train_features,
        test_features=test_features,
        predictions=predictions,
        probabilities=probabilities,
        classes=output_classes,
        classifier=classifier_model,
        metadata=metadata,
    )

normalize_feature_fit_scope(value)

Normalize feature-extractor fit-scope aliases to protocol categories.

Source code in src/neureptrace/decoding/precomputed_foundation.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def normalize_feature_fit_scope(value: str | None) -> str:
    """Normalize feature-extractor fit-scope aliases to protocol categories."""

    normalized = "external_frozen" if value is None else str(value).strip().lower().replace("-", "_")
    normalized = {
        "external": "external_frozen",
        "external_pretrained": "external_frozen",
        "frozen_external": "external_frozen",
        "frozen_pretrained": "external_frozen",
        "protocol_1": "source_only",
        "category_1": "source_only",
        "strict_source_only": "source_only",
        "source": "source_only",
        "train_only": "source_only",
        "source_target": "source_plus_unlabeled_target",
        "source_plus_target": "source_plus_unlabeled_target",
        "unlabeled_target": "source_plus_unlabeled_target",
        "target_adaptive": "source_plus_unlabeled_target",
        "protocol_2": "source_plus_unlabeled_target",
        "category_2": "source_plus_unlabeled_target",
        "few_shot": "target_calibrated",
        "calibrated": "target_calibrated",
        "target_calibration": "target_calibrated",
        "protocol_3": "target_calibrated",
        "category_3": "target_calibrated",
        "oracle": "oracle_target_included",
        "target_included": "oracle_target_included",
        "protocol_4": "oracle_target_included",
        "category_4": "oracle_target_included",
    }.get(normalized, normalized)
    if normalized not in FEATURE_FIT_SCOPES:
        raise ValueError(f"Unknown feature_fit_scope {value!r}. Available scopes: {', '.join(FEATURE_FIT_SCOPES)}.")
    return normalized