Skip to content

Data API

Data (General Container)

cobrabox.data.Data

Data(
    data: DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
)

Container for labelled multidimensional data.

Immutable container wrapping xarray.DataArray with metadata attributes. This is the most general data container with no assumptions about dimensions. For time-series data, use SignalData subclass instead.

Optional dimensions (examples): - time: Temporal dimension - space: Spatial dimension (electrode/voxel) - spaceX, spaceY, spaceZ: Additional spatial dimensions - run_index: Run/block index - window_index: Window index (e.g., from sliding window) - band_index: Frequency band index

Data type handling
  • Integer and float inputs are cast to float64
  • Complex inputs (e.g., from Hilbert transform) are preserved as complex128
Note

This class is immutable. To create modified versions, use methods that return new Data instances (e.g., features create new Data objects).

Initialize Data.

Parameters:

Name Type Description Default
data DataArray

xarray DataArray with arbitrary dimensions

required
sampling_rate float | None

Sampling rate in Hz (only meaningful for time-series data)

None
subjectID str | None

Subject identifier

None
groupID str | None

Group identifier

None
condition str | None

Experimental condition

None
history list[str] | None

List of operation names applied (default: empty list)

None
extra dict[str, Any] | None

Optional dict for additional fields and arrays (e.g. xr.DataArray, scalars)

None
Source code in src/cobrabox/data.py
def __init__(
    self,
    data: xr.DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> None:
    """Initialize Data.

    Args:
        data: xarray DataArray with arbitrary dimensions
        sampling_rate: Sampling rate in Hz (only meaningful for time-series data)
        subjectID: Subject identifier
        groupID: Group identifier
        condition: Experimental condition
        history: List of operation names applied (default: empty list)
        extra: Optional dict for additional fields and arrays (e.g. xr.DataArray, scalars)
    """
    if sampling_rate is not None and sampling_rate <= 0:
        raise ValueError("sampling_rate must be positive when provided")

    # Track if this data has a time dimension
    self._has_time = "time" in data.dims

    # Store xarray DataArray: preserve complex, convert int/float to float64
    if np.iscomplexobj(data.values):
        self._data = data.astype(np.complex128)
    else:
        self._data = data.astype(np.float64)

    # Store metadata in xarray attrs for persistence
    attrs = dict(data.attrs) if data.attrs else {}

    if subjectID is not None:
        attrs["subjectID"] = subjectID
    if groupID is not None:
        attrs["groupID"] = groupID
    if condition is not None:
        attrs["condition"] = condition

    # Initialize history if not provided
    if history is None:
        history = []
    attrs["history"] = history

    # Sampling rate: use provided value only if data has time dimension
    if sampling_rate is not None:
        if self._has_time:
            attrs["sampling_rate"] = sampling_rate
        # Silently ignore sampling_rate for non-time data
    elif self._has_time:
        # Try to infer from time coordinates only if time dimension exists
        inferred = self._infer_sampling_rate(data)
        if inferred is not None:
            attrs["sampling_rate"] = inferred

    # Update attrs
    self._data = self._data.assign_attrs(attrs)

    # User-defined extra fields and arrays (frozen copy)
    self._extra = dict(extra) if extra else {}
    self._frozen = True  # Mark as frozen after initialization

condition property

condition: str | None

Experimental condition.

data property

data: DataArray

Access underlying xarray DataArray.

dims property

dims: tuple[Hashable, ...]

Dimension names, in the order they appear in .shape.

Shortcut for .data.dims. See GH #119.

Example

import numpy as np import cobrabox as cb d = cb.Data.from_numpy(np.zeros((4, 200)), dims=["space", "time"]) d.dims ('space', 'time')

extra property

extra: dict[str, Any]

User-defined additional fields and arrays.

Returns a copy of the extra dict. To add fields, create a new Data object with updated extra dict.

groupID property

groupID: str | None

Group identifier.

history property

history: list[str]

List of operations applied to this dataset.

numpy property

numpy: ndarray

Access underlying numpy array directly, without copying.

Equivalent to .xarr.data / .data.data, provided as a transparent shortcut. See GH #112.

sampling_rate property

sampling_rate: float | None

Sampling rate in Hz.

Returns None if data has no time dimension or if sampling rate was not provided and could not be inferred.

shape property

shape: tuple[int, ...]

Length of each dimension, in dimension order, as numpy has it.

Shortcut for .data.shape, so you don't need to reach into xarray for the most common question asked of a container. Pair with .dims to learn which name goes with which length, or use .sizes to get both at once. See GH #119.

Example

import numpy as np import cobrabox as cb d = cb.Data.from_numpy(np.zeros((4, 200)), dims=["space", "time"]) d.shape (4, 200)

size property

size: int

Total number of elements, as numpy has it.

Shortcut for .data.size. Note this is the element count — for the per-dimension lengths use .shape or .sizes. See GH #119.

Example

import numpy as np import cobrabox as cb d = cb.Data.from_numpy(np.zeros((4, 200)), dims=["space", "time"]) d.size 800

sizes property

sizes: dict[Hashable, int]

Mapping of dimension name to its length.

Shortcut for dict(.data.sizes) — the xarray view of the shape, where each length is labelled. Returns a plain dict, so mutating it has no effect on this (immutable) container. See GH #119.

Example

import numpy as np import cobrabox as cb d = cb.Data.from_numpy(np.zeros((4, 200)), dims=["space", "time"]) d.sizes {'space': 4, 'time': 200}

subjectID property

subjectID: str | None

Subject identifier.

xarr property

xarr: DataArray

Access underlying xarray DataArray (alias for .data).

Prefer this over .data when the reader may not know whether they're holding a Data object or an xarray.DataArray — .data.data for the underlying numpy array reads as a typo, while .xarr.data (or .numpy) makes the intent explicit. See GH #112.

__copy__

__copy__() -> Data

Shallow copy — produces a new instance with the same underlying data.

Source code in src/cobrabox/data.py
def __copy__(self) -> Data:
    """Shallow copy — produces a new instance with the same underlying data."""
    return self._copy_slots(copy.copy)

__deepcopy__

__deepcopy__(memo: dict[int, Any]) -> Data

Deep copy — produces a new instance with independent copies of all data.

Source code in src/cobrabox/data.py
def __deepcopy__(self, memo: dict[int, Any]) -> Data:
    """Deep copy — produces a new instance with independent copies of all data."""
    new = self._copy_slots(lambda v: copy.deepcopy(v, memo))
    memo[id(self)] = new
    return new

__setattr__

__setattr__(name: str, value: Any) -> None

Prevent modification of attributes after initialization.

Source code in src/cobrabox/data.py
def __setattr__(self, name: str, value: Any) -> None:
    """Prevent modification of attributes after initialization."""
    if hasattr(self, "_frozen") and self._frozen:
        raise AttributeError(
            f"Cannot modify attribute '{name}'. Data is immutable. "
            f"Create a new Data instance instead."
        )
    super().__setattr__(name, value)

from_numpy classmethod

from_numpy(
    arr: ndarray,
    dims: list[Hashable],
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    extra: dict[str, Any] | None = None,
) -> Data

Create a Data object from a numpy array.

Creates a general Data container. For time-series data, use SignalData.from_numpy() which provides additional validation.

Parameters:

Name Type Description Default
arr ndarray

Numpy array with arbitrary shape

required
dims list[Hashable]

Dimension names matching array shape

required
sampling_rate float | None

Sampling rate in Hz (only used if 'time' in dims)

None
subjectID str | None

Subject identifier

None
groupID str | None

Group identifier

None
condition str | None

Experimental condition

None
extra dict[str, Any] | None

Optional extra dict

None

Returns:

Type Description
Data

Data instance

Raises:

Type Description
ValueError

If dims length doesn't match array ndim

Source code in src/cobrabox/data.py
@classmethod
def from_numpy(
    cls,
    arr: np.ndarray,
    dims: list[Hashable],
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    extra: dict[str, Any] | None = None,
) -> Data:
    """Create a Data object from a numpy array.

    Creates a general Data container. For time-series data, use
    SignalData.from_numpy() which provides additional validation.

    Args:
        arr: Numpy array with arbitrary shape
        dims: Dimension names matching array shape
        sampling_rate: Sampling rate in Hz (only used if 'time' in dims)
        subjectID: Subject identifier
        groupID: Group identifier
        condition: Experimental condition
        extra: Optional extra dict

    Returns:
        Data instance

    Raises:
        ValueError: If dims length doesn't match array ndim
    """
    arr = np.asarray(arr)

    if len(dims) != arr.ndim:
        raise ValueError("dims length must match array ndim")

    coords: dict[str, Any] = {}

    # Only create time coordinates if time dimension is present
    if "time" in dims:
        time_axis = dims.index("time")
        if sampling_rate is not None and sampling_rate > 0:
            coords["time"] = np.arange(arr.shape[time_axis], dtype=float) / sampling_rate
        else:
            coords["time"] = np.arange(arr.shape[time_axis], dtype=float)

    data = xr.DataArray(arr, dims=dims, coords=coords)
    return cls(
        data=data,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        extra=extra,
    )

from_xarray classmethod

from_xarray(
    ar: DataArray,
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> Data

Create a Data object from an xarray DataArray.

Creates a general Data container with arbitrary dimensions. For time-series data with validation, use SignalData.from_xarray().

Parameters:

Name Type Description Default
ar DataArray

xarray DataArray with dims (time, space) and optional coords.

required
sampling_rate float | None

Sampling rate in Hz. If not provided, inferred from time coordinates when they represent time in seconds.

None
subjectID str | None

Subject identifier.

None
groupID str | None

Group identifier.

None
condition str | None

Experimental condition.

None
history list[str] | None

List of operation names applied.

None
extra dict[str, Any] | None

Optional extra dict.

None

Returns:

Type Description
Data

Data instance

Example

ar = xr.DataArray(...) ds = cb.from_xarray(ar)

Source code in src/cobrabox/data.py
@classmethod
def from_xarray(
    cls,
    ar: xr.DataArray,
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> Data:
    """Create a Data object from an xarray DataArray.

    Creates a general Data container with arbitrary dimensions.
    For time-series data with validation, use SignalData.from_xarray().

    Args:
        ar: xarray DataArray with dims (time, space) and optional coords.
        sampling_rate: Sampling rate in Hz. If not provided, inferred from
            time coordinates when they represent time in seconds.
        subjectID: Subject identifier.
        groupID: Group identifier.
        condition: Experimental condition.
        history: List of operation names applied.
        extra: Optional extra dict.

    Returns:
        Data instance

    Example:
        >>> ar = xr.DataArray(...)
        >>> ds = cb.from_xarray(ar)
    """
    return cls(
        data=ar,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        history=history,
        extra=extra,
    )

to_numpy

to_numpy() -> ndarray
to_numpy(style: Literal['default']) -> ndarray
to_numpy(
    style: Literal["gorkastyle"],
) -> tuple[ndarray, ndarray, ndarray]
to_numpy(
    style: str = "default",
) -> ndarray | tuple[ndarray, ndarray, ndarray]

Convert data to numpy arrays.

Parameters:

Name Type Description Default
style str

Output style. "default" returns only data values. "gorkastyle" returns a tuple (time, space, labels) as separate arrays.

'default'

Returns:

Type Description
ndarray | tuple[ndarray, ndarray, ndarray]

Either the raw data array, or (time, space, labels).

Raises:

Type Description
ValueError

If style is unknown or if "gorkastyle" is used without required dimensions.

Source code in src/cobrabox/data.py
def to_numpy(
    self, style: str = "default"
) -> np.ndarray | tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Convert data to numpy arrays.

    Args:
        style: Output style. ``"default"`` returns only data values.
            ``"gorkastyle"`` returns a tuple
            ``(time, space, labels)`` as separate arrays.

    Returns:
        Either the raw data array, or ``(time, space, labels)``.

    Raises:
        ValueError: If style is unknown or if "gorkastyle" is used
            without required dimensions.
    """
    if style == "default":
        return self._data.to_numpy()
    if style == "gorkastyle":
        if "time" not in self._data.dims:
            raise ValueError("gorkastyle requires 'time' dimension")
        if "space" not in self._data.dims:
            raise ValueError("gorkastyle requires 'space' dimension")
        time = np.asarray(self._data.coords["time"].values)
        space = np.asarray(self._data.coords["space"].values)
        labels = np.asarray(self._data.values)
        return time, space, labels
    raise ValueError("Unknown style. Expected 'default' or 'gorkastyle'.")

to_pandas

to_pandas(name: str | None = None) -> DataFrame

Convert to pandas DataFrame.

Returns:

Type Description
DataFrame

pandas DataFrame with MultiIndex from dimensions

Source code in src/cobrabox/data.py
def to_pandas(self, name: str | None = None) -> pd.DataFrame:
    """Convert to pandas DataFrame.

    Returns:
        pandas DataFrame with MultiIndex from dimensions
    """
    return self._data.to_dataframe(name=name or "data")

SignalData (Time-Series Container)

cobrabox.data.SignalData

SignalData(
    data: DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
)

Bases: Data

Container for time-series data with mandatory time dimension.

SignalData extends Data with the requirement that data must have a 'time' dimension. This is appropriate for EEG, fMRI, and other time-series data where temporal ordering matters.

Mandatory dimensions
  • time: Temporal dimension

Optional dimensions (examples): - space: Spatial dimension (electrode/voxel) - spaceX, spaceY, spaceZ: Additional spatial dimensions - run_index: Run/block index - window_index: Window index (e.g., from sliding window) - band_index: Frequency band index

Note

This class is immutable. To create modified versions, use methods that return new Data instances (e.g., features create new Data objects).

Initialize SignalData.

Parameters:

Name Type Description Default
data DataArray

xarray DataArray with at least 'time' dimension

required
sampling_rate float | None

Sampling rate in Hz. If not provided, inferred from time coordinates when they represent time in seconds.

None
subjectID str | None

Subject identifier

None
groupID str | None

Group identifier

None
condition str | None

Experimental condition

None
history list[str] | None

List of operation names applied (default: empty list)

None
extra dict[str, Any] | None

Optional dict for additional fields and arrays

None

Raises:

Type Description
ValueError

If data does not have 'time' dimension

Source code in src/cobrabox/data.py
def __init__(
    self,
    data: xr.DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> None:
    """Initialize SignalData.

    Args:
        data: xarray DataArray with at least 'time' dimension
        sampling_rate: Sampling rate in Hz. If not provided, inferred from
            time coordinates when they represent time in seconds.
        subjectID: Subject identifier
        groupID: Group identifier
        condition: Experimental condition
        history: List of operation names applied (default: empty list)
        extra: Optional dict for additional fields and arrays

    Raises:
        ValueError: If data does not have 'time' dimension
    """
    # Validate mandatory time dimension
    if "time" not in data.dims:
        raise ValueError("SignalData must have 'time' dimension")

    # optimisation: time dimension is always last for SignalData
    data = data.transpose(..., "time")

    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        history=history,
        extra=extra,
    )

from_numpy classmethod

from_numpy(
    arr: ndarray,
    dims: list[Hashable],
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    extra: dict[str, Any] | None = None,
) -> SignalData

Create a SignalData object from a numpy array.

Requires at least 1 dimension and that resulting dims include 'time'.

Parameters:

Name Type Description Default
arr ndarray

Numpy array

required
dims list[Hashable]

Dimension names matching array shape, must include 'time'

required
sampling_rate float | None

Sampling rate in Hz

None
subjectID str | None

Subject identifier

None
groupID str | None

Group identifier

None
condition str | None

Experimental condition

None
extra dict[str, Any] | None

Optional extra dict

None

Returns:

Type Description
SignalData

SignalData instance

Raises:

Type Description
ValueError

If dims doesn't include 'time' or dims length mismatch

Source code in src/cobrabox/data.py
@classmethod
def from_numpy(
    cls,
    arr: np.ndarray,
    dims: list[Hashable],
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    extra: dict[str, Any] | None = None,
) -> SignalData:
    """Create a SignalData object from a numpy array.

    Requires at least 1 dimension and that resulting dims include 'time'.

    Args:
        arr: Numpy array
        dims: Dimension names matching array shape, must include 'time'
        sampling_rate: Sampling rate in Hz
        subjectID: Subject identifier
        groupID: Group identifier
        condition: Experimental condition
        extra: Optional extra dict

    Returns:
        SignalData instance

    Raises:
        ValueError: If dims doesn't include 'time' or dims length mismatch
    """
    arr = np.asarray(arr)

    if "time" not in dims:
        raise ValueError("dims must include 'time' for SignalData")

    time_axis = dims.index("time")

    coords: dict[str, Any] = {}
    if sampling_rate is not None and sampling_rate > 0:
        coords["time"] = np.arange(arr.shape[time_axis], dtype=float) / sampling_rate
    else:
        coords["time"] = np.arange(arr.shape[time_axis], dtype=float)

    data = xr.DataArray(arr, dims=dims, coords=coords)
    return cls(
        data=data,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        extra=extra,
    )

from_xarray classmethod

from_xarray(
    ar: DataArray,
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> SignalData

Create a SignalData object from an xarray DataArray.

The DataArray must have 'time' dimension. Time coordinates should be in seconds if you want sampling_rate to be inferred.

Parameters:

Name Type Description Default
ar DataArray

xarray DataArray with 'time' dimension

required
sampling_rate float | None

Sampling rate in Hz. If not provided, inferred from time coordinates when they represent time in seconds.

None
subjectID str | None

Subject identifier

None
groupID str | None

Group identifier

None
condition str | None

Experimental condition

None
history list[str] | None

List of operation names applied

None
extra dict[str, Any] | None

Optional extra dict

None

Returns:

Type Description
SignalData

SignalData instance

Raises:

Type Description
ValueError

If DataArray doesn't have 'time' dimension

Example

ar = xr.DataArray(...) ds = SignalData.from_xarray(ar)

Source code in src/cobrabox/data.py
@classmethod
def from_xarray(
    cls,
    ar: xr.DataArray,
    *,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> SignalData:
    """Create a SignalData object from an xarray DataArray.

    The DataArray must have 'time' dimension. Time coordinates
    should be in seconds if you want sampling_rate to be inferred.

    Args:
        ar: xarray DataArray with 'time' dimension
        sampling_rate: Sampling rate in Hz. If not provided, inferred from
            time coordinates when they represent time in seconds.
        subjectID: Subject identifier
        groupID: Group identifier
        condition: Experimental condition
        history: List of operation names applied
        extra: Optional extra dict

    Returns:
        SignalData instance

    Raises:
        ValueError: If DataArray doesn't have 'time' dimension

    Example:
        >>> ar = xr.DataArray(...)
        >>> ds = SignalData.from_xarray(ar)
    """
    return cls(
        data=ar,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        history=history,
        extra=extra,
    )

EEG (Type Marker)

cobrabox.data.EEG

EEG(
    data: DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
    ref_channel: str | None = None,
)

Bases: SignalData

EEG data container.

EEG is a time-series signal data type with mandatory time dimension. Typically also includes 'space' dimension representing electrodes/channels.

Parameters:

Name Type Description Default
ref_channel str | None

The reference used for this recording. One of:

  • None — reference unspecified (default).
  • 'average' — common average reference.
  • 'bipolar' — channels are already bipolar (re-referenced against adjacent electrodes).
  • any channel name that appears in the 'space' coordinate — single-electrode reference.
None

Raises:

Type Description
ValueError

If ref_channel is not None, 'average', 'bipolar', or a channel present in the 'space' coordinate.

Source code in src/cobrabox/data.py
def __init__(
    self,
    data: xr.DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
    ref_channel: str | None = None,
) -> None:
    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        history=history,
        extra=extra,
    )
    _RESERVED = {"average", "bipolar"}
    if ref_channel is not None and ref_channel not in _RESERVED:
        space_coords = list(data.coords["space"].values) if "space" in data.coords else []
        if ref_channel not in space_coords:
            raise ValueError(
                f"ref_channel {ref_channel!r} must be 'average', "
                f"'bipolar', or one of the space coords: {space_coords}."
            )
    object.__setattr__(self, "ref_channel", ref_channel)

FMRI (Type Marker)

cobrabox.data.FMRI

FMRI(
    data: DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
)

Bases: SignalData

fMRI data container.

FMRI is a time-series signal data type with mandatory time dimension. Typically also includes spatial dimensions representing voxels.

Source code in src/cobrabox/data.py
def __init__(
    self,
    data: xr.DataArray,
    sampling_rate: float | None = None,
    subjectID: str | None = None,
    groupID: str | None = None,
    condition: str | None = None,
    history: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> None:
    """Initialize SignalData.

    Args:
        data: xarray DataArray with at least 'time' dimension
        sampling_rate: Sampling rate in Hz. If not provided, inferred from
            time coordinates when they represent time in seconds.
        subjectID: Subject identifier
        groupID: Group identifier
        condition: Experimental condition
        history: List of operation names applied (default: empty list)
        extra: Optional dict for additional fields and arrays

    Raises:
        ValueError: If data does not have 'time' dimension
    """
    # Validate mandatory time dimension
    if "time" not in data.dims:
        raise ValueError("SignalData must have 'time' dimension")

    # optimisation: time dimension is always last for SignalData
    data = data.transpose(..., "time")

    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        subjectID=subjectID,
        groupID=groupID,
        condition=condition,
        history=history,
        extra=extra,
    )