Skip to content

Neuro

Neural-domain primitives for VneuroTK.

NeuroData

Neural signal container with trial-structured views.

Holds the raw neural array plus optional trial-boundary information, and exposes epochs and continuous views derived from that structure. NeuroData is not a NumPy array subclass — use :attr:data for the raw array when NumPy operations are needed.

Parameters:

Name Type Description Default
data ndarray

Raw neural array.

required
trial_starts ndarray or None

Start sample index per trial.

None
trial_ends ndarray or None

End sample index per trial.

None
data_mode str or None

"continuous", "epochs", or "patterns".

None

Examples:

>>> import numpy as np
>>> nd = NeuroData(np.random.randn(1000, 64))
>>> nd.shape
(1000, 64)
>>> nd.data[:100]   # plain ndarray slice
Source code in src/vneurotk/neuro/base.py
class NeuroData:
    """Neural signal container with trial-structured views.

    Holds the raw neural array plus optional trial-boundary information, and
    exposes ``epochs`` and ``continuous`` views derived from that structure.
    ``NeuroData`` is *not* a NumPy array subclass — use :attr:`data` for the
    raw array when NumPy operations are needed.

    Parameters
    ----------
    data : np.ndarray
        Raw neural array.
    trial_starts : np.ndarray or None
        Start sample index per trial.
    trial_ends : np.ndarray or None
        End sample index per trial.
    data_mode : str or None
        ``"continuous"``, ``"epochs"``, or ``"patterns"``.

    Examples
    --------
    >>> import numpy as np
    >>> nd = NeuroData(np.random.randn(1000, 64))
    >>> nd.shape
    (1000, 64)
    >>> nd.data[:100]   # plain ndarray slice
    """

    def __init__(
        self,
        data: np.ndarray,
        trial_starts: np.ndarray | None = None,
        trial_ends: np.ndarray | None = None,
        data_mode: str | None = None,
    ) -> None:
        self._data = np.asarray(data)
        self._trial_starts = trial_starts
        self._trial_ends = trial_ends
        self._data_mode = data_mode

    # ------------------------------------------------------------------
    # Raw data access
    # ------------------------------------------------------------------

    @property
    def data(self) -> np.ndarray:
        """Raw neural array as a plain :class:`numpy.ndarray`."""
        return self._data

    # ------------------------------------------------------------------
    # ndarray-compatible attribute proxies
    # ------------------------------------------------------------------

    @property
    def shape(self) -> tuple:
        """Shape of the underlying data array."""
        return self._data.shape

    @property
    def ndim(self) -> int:
        """Number of dimensions of the underlying data array."""
        return self._data.ndim

    @property
    def dtype(self) -> np.dtype:
        """Data type of the underlying data array."""
        return self._data.dtype

    @property
    def size(self) -> int:
        """Total number of elements."""
        return self._data.size

    def __array__(self, dtype=None) -> np.ndarray:
        """Support ``np.asarray(neuro_data)``."""
        if dtype is not None:
            return self._data.astype(dtype)
        return self._data

    # ------------------------------------------------------------------
    # Trial-structured views
    # ------------------------------------------------------------------

    @property
    def epochs(self) -> np.ndarray:
        """Trial-epoched view, shape ``(n_trials, n_timepoints, nchan)``.

        If the underlying data is already in epochs format it is returned
        as-is with a warning.

        Raises
        ------
        RuntimeError
            If trial structure is not available (call ``BaseData.configure()`` first).
        """
        if self._trial_starts is None:
            raise RuntimeError("No trial structure. Call BaseData.configure() first.")

        if self._data_mode == "epochs":
            logger.warning(
                "neuro is already in epochs format (shape {}), returning as-is",
                self._data.shape,
            )
            return self._data

        return np.stack([self._data[s:e] for s, e in zip(self._trial_starts, self._trial_ends, strict=True)])  # ty: ignore[not-iterable, invalid-argument-type]

    @property
    def continuous(self) -> np.ndarray:
        """Concatenated-trials view, shape ``(total_trial_samples, nchan)``.

        If the underlying data is already in continuous format it is returned
        as-is with a warning.

        Raises
        ------
        RuntimeError
            If trial structure is not available (call ``BaseData.configure()`` first).
        """
        if self._trial_starts is None:
            raise RuntimeError("No trial structure. Call BaseData.configure() first.")

        if self._data_mode == "continuous":
            logger.warning(
                "neuro is already in continuous format (shape {}), returning as-is",
                self._data.shape,
            )
            return self._data

        if self._data_mode == "epochs":
            return self._data.reshape(-1, self._data.shape[-1])

        return np.concatenate([self._data[s:e] for s, e in zip(self._trial_starts, self._trial_ends, strict=True)])  # ty: ignore[not-iterable, invalid-argument-type]

    # ------------------------------------------------------------------
    # Display
    # ------------------------------------------------------------------

    def _has_trial_structure(self) -> bool:
        return self._trial_starts is not None

    def __repr__(self) -> str:
        mode = self._data_mode or "unknown"
        ts = "configured" if self._has_trial_structure() else "not configured"
        n_trials = len(self._trial_starts) if self._trial_starts is not None else 0
        return f"NeuroData  shape={self._data.shape}  mode={mode}" + (
            f"  trials={n_trials}" if n_trials else f"  trials={ts}"
        )

    def _repr_html_(self) -> str:
        mode = self._data_mode or "unknown"
        has_ts = self._has_trial_structure()
        n_trials = len(self._trial_starts) if self._trial_starts is not None else None

        ok = '<span class="nd-ok">✓ available</span>'
        na = '<span class="nd-na">not configured</span>'

        rows = [
            ("Shape", str(self._data.shape)),
            ("Mode", mode),
            ("Trials", str(n_trials) if n_trials is not None else "—"),
            ("Channels", str(self._data.shape[-1]) if self._data.ndim >= 2 else "—"),
            (".epochs", ok if has_ts else na),
            (".continuous", ok if has_ts else na),
        ]
        trs = "".join(f"<tr><th>{k}</th><td>{v}</td></tr>" for k, v in rows)
        table = f"<table>{trs}</table>"
        return (
            f'{_STYLE}<div class="nd-info">'
            f"<details open><summary><strong>NeuroData</strong></summary>{table}</details>"
            f"</div>"
        )

continuous property

Concatenated-trials view, shape (total_trial_samples, nchan).

If the underlying data is already in continuous format it is returned as-is with a warning.

Raises:

Type Description
RuntimeError

If trial structure is not available (call BaseData.configure() first).

data property

Raw neural array as a plain :class:numpy.ndarray.

dtype property

Data type of the underlying data array.

epochs property

Trial-epoched view, shape (n_trials, n_timepoints, nchan).

If the underlying data is already in epochs format it is returned as-is with a warning.

Raises:

Type Description
RuntimeError

If trial structure is not available (call BaseData.configure() first).

ndim property

Number of dimensions of the underlying data array.

shape property

Shape of the underlying data array.

size property

Total number of elements.

__array__(dtype=None)

Support np.asarray(neuro_data).

Source code in src/vneurotk/neuro/base.py
def __array__(self, dtype=None) -> np.ndarray:
    """Support ``np.asarray(neuro_data)``."""
    if dtype is not None:
        return self._data.astype(dtype)
    return self._data

TrialStructure

Value object produced by the trial-structure factory functions.

All fields are written atomically by :meth:BaseData._apply_trial_structure.

Source code in src/vneurotk/neuro/trial.py
@dataclass
class TrialStructure:
    """Value object produced by the trial-structure factory functions.

    All fields are written atomically by :meth:`BaseData._apply_trial_structure`.
    """

    stim_labels: np.ndarray
    trial: np.ndarray
    trial_starts: np.ndarray
    trial_ends: np.ndarray
    vision_onsets: np.ndarray
    vision_info: dict
    trial_info: dict

Builders

Build a :class:TrialStructure for pre-epoched recordings.

Parameters:

Name Type Description Default
visual_ids ndarray

Stimulus ID per trial, shape (n_trials,).

required
vision_onsets ndarray or None

Per-trial onset offsets within each epoch.

required
neuro_shape tuple

Shape of the neuro array (n_trials, n_timebins, ...).

required
existing_vision_onsets ndarray or None

Fallback: onsets already stored on the Recording before this call.

None

Returns:

Type Description
TrialStructure
Source code in src/vneurotk/neuro/trial.py
def build_trial_structure_epochs(
    visual_ids: np.ndarray,
    vision_onsets: np.ndarray | None,
    neuro_shape: tuple,
    existing_vision_onsets: np.ndarray | None = None,
) -> TrialStructure:
    """Build a :class:`TrialStructure` for pre-epoched recordings.

    Parameters
    ----------
    visual_ids : np.ndarray
        Stimulus ID per trial, shape ``(n_trials,)``.
    vision_onsets : np.ndarray or None
        Per-trial onset offsets within each epoch.
    neuro_shape : tuple
        Shape of the neuro array ``(n_trials, n_timebins, ...)``.
    existing_vision_onsets : np.ndarray or None
        Fallback: onsets already stored on the Recording before this call.

    Returns
    -------
    TrialStructure
    """
    n_trials = neuro_shape[0]
    n_timebins = neuro_shape[1]

    if vision_onsets is not None:
        vision_onsets = np.asarray(vision_onsets, dtype=int)
    elif existing_vision_onsets is not None:
        vision_onsets = existing_vision_onsets
    else:
        vision_onsets = np.zeros(n_trials, dtype=int)
        logger.warning("epochs data has no vision_onsets, defaulting to index 0 of each epoch")

    stim_labels = _stim_labels_epochs(n_trials, n_timebins, vision_onsets, visual_ids)
    trial = np.stack([np.full(n_timebins, i, dtype=float) for i in range(n_trials)])
    vision_info = _build_vision_info(visual_ids)
    logger.info("Configured (epochs): {} trials, {} unique stimuli", n_trials, vision_info["n_stim"])
    return TrialStructure(
        stim_labels=stim_labels,
        trial=trial,
        trial_starts=np.zeros(n_trials, dtype=int),
        trial_ends=np.full(n_trials, n_timebins, dtype=int),
        vision_onsets=vision_onsets,
        vision_info=vision_info,
        trial_info={
            "baseline": [-int(vision_onsets[0]), 0],
            "trial_window": [-int(vision_onsets[0]), n_timebins - int(vision_onsets[0])],
        },
    )

Build a :class:TrialStructure for continuous (raw) recordings.

Parameters:

Name Type Description Default
visual_ids ndarray

Stimulus ID per onset, shape (n_onsets,).

required
trial_window list of float | int

Two-element [start, end] relative to each onset. Float values are seconds; int values are samples.

required
vision_onsets ndarray

Onset sample indices, shape (n_onsets,).

required
ntime int

Total number of time samples in the recording.

required
sfreq float

Sampling frequency in Hz.

required

Returns:

Type Description
TrialStructure
Source code in src/vneurotk/neuro/trial.py
def build_trial_structure_continuous(
    visual_ids: np.ndarray,
    trial_window: list[float | int],
    vision_onsets: np.ndarray,
    ntime: int,
    sfreq: float,
) -> TrialStructure:
    """Build a :class:`TrialStructure` for continuous (raw) recordings.

    Parameters
    ----------
    visual_ids : np.ndarray
        Stimulus ID per onset, shape ``(n_onsets,)``.
    trial_window : list of float | int
        Two-element ``[start, end]`` relative to each onset.
        Float values are seconds; int values are samples.
    vision_onsets : np.ndarray
        Onset sample indices, shape ``(n_onsets,)``.
    ntime : int
        Total number of time samples in the recording.
    sfreq : float
        Sampling frequency in Hz.

    Returns
    -------
    TrialStructure
    """
    vision_onsets = np.asarray(vision_onsets, dtype=int)
    tw_samples = _window_to_samples(trial_window, sfreq)
    trial_starts = vision_onsets + tw_samples[0]
    trial_ends = vision_onsets + tw_samples[1]
    stim_labels = _stim_labels_continuous(ntime, vision_onsets, visual_ids)
    trial = np.full(ntime, np.nan)
    for i, (ts, te) in enumerate(zip(trial_starts, trial_ends, strict=True)):
        trial[ts:te] = i
    vision_info = _build_vision_info(visual_ids)
    logger.info(
        "Configured (raw): {} trials, {} unique stimuli",
        len(trial_starts),
        vision_info["n_stim"],
    )
    return TrialStructure(
        stim_labels=stim_labels,
        trial=trial,
        trial_starts=trial_starts,
        trial_ends=trial_ends,
        vision_onsets=vision_onsets,
        vision_info=vision_info,
        trial_info={"baseline": [tw_samples[0], 0], "trial_window": tw_samples},
    )