pyntbci.classifiers.UnsupervisedRCCA

class pyntbci.classifiers.UnsupervisedRCCA(stimulus: NDArray, fs: int, event: str = 'duration', onset_event: bool = True, decoding_length: float = None, decoding_stride: float = None, encoding_length: float | list[float] = 0.3, encoding_stride: float | list[float] = None, latency: NDArray = None, tmin: float = 0, n_components: int = 1, gamma_x: float | list[float] | NDArray = None, gamma_m: float | list[float] | NDArray = None, alpha_x: float = None, alpha_m: float = None, cumulative: bool = True, confidence: bool = False, posthoc: bool = False, response_prior: NDArray = None, response_prior_gamma: float = 1.0, smoothness_m: float = None)[source]

Unsupervised adaptive reconvolution CCA classifier for calibration-free decoding (Thielen et al. 2021, 2024, 2025, 2026).

Instead of a supervised calibration, each trial is decoded by fitting a separate rCCA per candidate stimulus (as a hypothesis) and selecting the stimulus whose model best fits the trial, i.e. yields the highest correlation between the spatially filtered EEG and the temporally filtered stimulus structure matrix. This is the instantaneous mode (cumulative=False), which treats every trial independently.

Three cumulative extensions build a model from previously decoded trials, using their predicted labels as pseudo-labels (there are no ground-truth labels in a calibration-free setting):

  • cumulative=True: every hypothesis is fit on all previously decoded trials (at their pseudo-labels) plus the current trial (hypothesized as each candidate). This is mathematically identical to refitting from scratch on the full history every trial, but is done efficiently by keeping a single running covariance of the pseudo-labeled history (see RunningCovariance in utilities) shared across all hypotheses, so each trial only adds its own (bounded) contribution rather than reprocessing the whole history. The first trial, with no history, reduces to the instantaneous mode.

  • confidence=True (implies cumulative): each trial is weighted by a confidence, so that high-confidence trials drive the model updates and low-confidence trials are suppressed. The confidence is the normalized correlation margin (rho_winner - rho_runner_up) / std(rho_except_winner), estimated from an instantaneous pass, and used as a per-trial weight in the running covariance.

  • posthoc=True (implies cumulative): after each trial, all previously decoded trials are re-decoded with the just-updated (presumably better) model and their pseudo-labels are corrected, which then affects subsequent updates. This is the only mode that must retain the past trials’ EEG (in X_hist_), since re-decoding needs the raw data; a changed label is applied to the running covariance as an exact remove-then-re-add, avoiding a full refit. The other modes keep no raw data (only the running covariance and the list of pseudo-labels).

These flags reproduce the four variants: instantaneous (all False), cumulative (cumulative), confidence-weighted cumulative (cumulative, confidence), and confidence-weighted cumulative with post hoc re-analysis (cumulative, confidence, posthoc).

Note, decoding is inherently online and stateful: predict() streams trials in their given (chronological) order, decoding each with the model learned from the ones before it, and the internal session persists across calls. Decoding trials one at a time therefore accumulates exactly as decoding them in one call does (as needed for real-time use where trials arrive one by one): [predict(X[[i]]) for i in range(n_trials)] gives the same result as predict(X). Because state persists, predict()/decision_function() are not pure functions; call fit() (or pass reset=True) to start a fresh session, e.g. before an independent replay. The single-trial partial_fit_predict() is the same online step exposed directly. The structure-matrix machinery of rCCA (event and encoding matrices, latency correction, optional spatio-spectral decoding matrix) is reused, and the core CCA is solved with the same whitened-SVD as CCA in transformers (the EEG-side whitening is shared across all candidate hypotheses of a trial, since only the stimulus side differs). With short trials or a wide encoding matrix, the per-hypothesis covariances can be ill-conditioned; set gamma_x/gamma_m (or alpha_x/alpha_m) to regularize, as for supervised rCCA.

Parameters:
  • stimulus (NDArray) – The stimulus used for stimulation of shape (n_classes, n_samples). Should be sampled at fs. One cycle (i.e., one stimulus-repetition) is sufficient.

  • fs (int) – The sampling frequency of the EEG data in Hz.

  • event (str (default: "duration")) – The event definition to map stimulus to events.

  • onset_event (bool (default: True)) – Whether to add an event for the onset of stimulation. Added as last event.

  • decoding_length (float (default: None)) – The length of the spectral filter for each data channel in seconds. If None, it is set to 1/fs, equivalent to 1 sample, such that no phase-shifting is performed and thus no (spatio-)spectral filter is learned.

  • decoding_stride (float (default: None)) – The stride of the spectral filter for each data channel in seconds. If None, it is set to 1/fs.

  • encoding_length (float | list[float] (default: 0.3)) – The length of the transient response(s) for each of the events in seconds.

  • encoding_stride (float | list[float] (default: None)) – The stride of the transient response(s) for each of the events in seconds. If None, it is set to 1/fs.

  • latency (NDArray (default: None)) – The raster latencies of each of the classes of shape (n_classes,) that the templates need to be corrected for.

  • tmin (float (default: 0)) – The start of stimulation in seconds. Can be used if there was a delay in the marker.

  • n_components (int (default: 1)) – The number of CCA components to use. Decoding and confidence use the first component only.

  • gamma_x (float | list[float] | NDArray (default: None)) – Regularization on the covariance matrix for CCA along X (channels), see rCCA.

  • gamma_m (float | list[float] | NDArray (default: None)) – Regularization on the covariance matrix for CCA along M (samples), see rCCA.

  • alpha_x (float (default: None)) – Amount of variance to retain in computing the inverse of the covariance matrix of X. If None, all variance.

  • alpha_m (float (default: None)) – Amount of variance to retain in computing the inverse of the covariance matrix of M. If None, all variance.

  • cumulative (bool (default: True)) – Whether to learn cumulatively from previously decoded trials (using their pseudo-labels). If False, each trial is decoded instantaneously and independently.

  • confidence (bool (default: False)) – Whether to weight each trial by its confidence during cumulative updates. Implies cumulative.

  • posthoc (bool (default: False)) – Whether to re-decode and relabel all previous trials after each update. Implies cumulative, and retains the past trials’ EEG in X_hist_.

  • response_prior (NDArray (default: None)) – A prior on the expected transient response (e.g. a flash-VEP: a negative peak near 75 ms, a positive peak near 100 ms, and a negative peak near 125 ms), sampled at fs. Either one response of length n_event_samples (applied to every event) or the full concatenation of the per-event responses of length n_features (matching the temporal filter r_; see encoding_length). If given, the learned response is softly regularized toward it (see response_prior_gamma), which anchors the response’s absolute phase. This is what makes decoding work for circularly-shifted codes (e.g. shifted m-sequences): without it, an unconstrained response can circularly slide to make every candidate stimulus fit equally well (the more so the longer encoding_length), so the classes become indistinguishable. If None (default), no prior is used.

  • response_prior_gamma (float (default: 1.0)) – The strength of the soft regularization toward response_prior, ranging from 0 (ignore the prior, purely data-driven) upwards (larger pulls the response more strongly toward the prior; in the limit the response equals the prior). Only used if response_prior is not None.

  • smoothness_m (float (default: None)) – The strength of a temporal-smoothness prior on the response, penalizing the squared differences between adjacent response samples (see smoothness_matrix in utilities and rCCA’s smoothness_m), so the response is smooth. Unlike response_prior it makes no assumption about the response shape, so it does not by itself resolve circularly-shifted codes; it composes with response_prior (which anchors the phase) and reduces overfitting. Ranges from 0 (no smoothing) upwards. If None (default), no smoothness prior is used.

classes_

The class labels of shape (n_classes,).

Type:

NDArray

events_

The list of events used to map the stimulus to, as set by the internal rCCA.

Type:

list

labels_

The pseudo-labels (predicted labels) of the decoded trials, in order.

Type:

list

confidences_

The confidence of each decoded trial, in order.

Type:

list

w_

The spatial filter of the most recently winning model of shape (n_channels, n_components).

Type:

NDArray

r_

The temporal filter of the most recently winning model of shape (n_features, n_components).

Type:

NDArray

cov_

The running covariance of the pseudo-labeled history (only populated if cumulative).

Type:

RunningCovariance

X_hist_

The (decoded) EEG of the decoded trials, retained only if posthoc, for re-decoding.

Type:

list

References

Thielen, J., Marsman, P., Farquhar, J., & Desain, P. (2021). From full calibration to zero training for a code-modulated visual evoked potentials for brain–computer interface. Journal of Neural Engineering, 18(5), 056007. doi: https://doi.org/10.1088/1741-2552/abecef Thielen, J., Sosulski, J., & Tangermann, M. (2024). Exploring new territory: Calibration-free decoding for c-VEP BCI. 9th Graz Brain-Computer Interface Conference 2024, 325–330. doi: https://doi.org/10.3217/978-3-99161-014-4-057 Thielen, J., & Tangermann, M. (2025). Exploring new territory II: Calibration-free decoding for ERP BCI. In 2025 IEEE International Conference on Systems, Man, and Cybernetics (SMC) (pp. 3788-3793). IEEE. doi: https://doi.org/10.1109/SMC58881.2025.11342596 Thielen, J. (2026). Confidence-weighted cumulative rCCA with post hoc re-analysis: unsupervised adaptive learning for calibration-free c-VEP BCI. 10th Graz Brain-Computer Interface Conference 2026. doi:

decision_function(X: NDArray, reset: bool = False, update: bool = True) NDArray[source]

Decode a sequence of trials online and return the per-trial per-class correlation scores.

Stateful and online, see predict() (of which this is the score-returning counterpart).

Parameters:
  • X (NDArray) – The EEG data of shape (n_trials, n_channels, n_samples), in chronological order.

  • reset (bool (default: False)) – Whether to discard the current online session and start fresh before decoding X, see predict().

  • update (bool (default: True)) – Whether to commit the decoded trials to the online model, see predict(). Use update=False for a pure, side-effect-free scoring (e.g. probing growing segments of a trial in a dynamic-stopping loop).

Returns:

scores – The per-trial per-class correlation scores of shape (n_trials, n_classes).

Return type:

NDArray

fit(X: NDArray = None, y: NDArray = None) ClassifierMixin[source]

Set up the classifier (calibration-free: no training data or labels are used).

Parameters:
  • X (NDArray (default: None)) – Not used, present for scikit-learn API consistency.

  • y (NDArray (default: None)) – Not used, present for scikit-learn API consistency.

Returns:

self – Returns the instance itself.

Return type:

ClassifierMixin

partial_fit_predict(X: NDArray, update: bool = True) tuple[int, float, NDArray][source]

Decode a single trial online, optionally committing it to the model with its pseudo-label.

Parameters:
  • X (NDArray) – The EEG data of a single trial of shape (n_channels, n_samples) or (1, n_channels, n_samples).

  • update (bool (default: True)) – Whether to commit this trial to the online model (updating the running covariance and the pseudo-label/confidence/history state) after decoding it. If False, the trial is decoded against the current model but no state is changed, i.e. a pure, side-effect-free query that can be repeated any number of times on the same or a growing trial (as a dynamic-stopping loop does, decoding growing segments of a trial until it decides to stop) without polluting the model; commit the decided trial once afterwards with a single update=True call. Only meaningful for cumulative variants (with cumulative=False there is no cross-trial state to update).

Returns:

  • label (int) – The predicted (pseudo-) label of the trial.

  • confidence (float) – The confidence of the prediction.

  • scores (NDArray) – The per-class correlation scores of shape (n_classes,).

predict(X: NDArray, reset: bool = False, update: bool = True) NDArray[source]

Decode a sequence of trials online, in the given (chronological) order.

Each trial is decoded with the model learned from all trials decoded so far, and then folds into that model (using its own prediction as a pseudo-label) if cumulative. This is a stateful, online operation: the internal session persists across calls, so decoding trials one at a time is equivalent to decoding them in a single call, i.e. [predict(X[[i]]) for i in range(n_trials)] gives the same result as predict(X), as needed for real-time use where trials arrive one by one. Because it persists state, predict() is not a pure function: call fit() (or pass reset=True) to start a fresh session, e.g. before an independent replay.

Parameters:
  • X (NDArray) – The EEG data of shape (n_trials, n_channels, n_samples), in chronological order.

  • reset (bool (default: False)) – Whether to discard the current online session and start fresh before decoding X. Use reset=True (or a fresh instance, or fit()) for a self-contained replay; leave False to continue an ongoing session.

  • update (bool (default: True)) – Whether to commit the decoded trials to the online model. Use update=False for a pure, side-effect-free decode (nothing is committed), e.g. to repeatedly probe growing segments of a trial in a dynamic-stopping loop without polluting the model, then commit the decided trial once with update=True. See partial_fit_predict.

Returns:

y – The predicted labels of shape (n_trials,).

Return type:

NDArray

set_decision_function_request(*, reset: bool | None | str = '$UNCHANGED$', update: bool | None | str = '$UNCHANGED$') UnsupervisedRCCA

Configure whether metadata should be requested to be passed to the decision_function method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to decision_function if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to decision_function.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters:
  • reset (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for reset parameter in decision_function.

  • update (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for update parameter in decision_function.

Returns:

self – The updated object.

Return type:

object

set_predict_request(*, reset: bool | None | str = '$UNCHANGED$', update: bool | None | str = '$UNCHANGED$') UnsupervisedRCCA

Configure whether metadata should be requested to be passed to the predict method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to predict.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters:
  • reset (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for reset parameter in predict.

  • update (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for update parameter in predict.

Returns:

self – The updated object.

Return type:

object

set_score_request(*, sample_weight: bool | None | str = '$UNCHANGED$') UnsupervisedRCCA

Configure whether metadata should be requested to be passed to the score method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to score if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to score.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters:

sample_weight (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for sample_weight parameter in score.

Returns:

self – The updated object.

Return type:

object