Source code for ibl_bwb_eval.tasks.ts1

"""TS1: the task vocabulary, and what each task predicts.

Part of the evaluation contract rather than of any trainer, so the scorer, the TS1
suite and the multi-task pretraining datasets all read the same definition.
"""

from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
from typing import Literal, TypeAlias, get_args

from ibl_bwb_eval.tasks.types import DataType, TargetResolution

TS1Task: TypeAlias = Literal[
    "choice",
    "reward",
    "stimulus_contrast",
    "whisker_motion_energy",
    "wheel_speed",
    "right_paw_speed",
    "left_paw_speed",
    "licking_rate",
]
"""The eight scored TS1 tasks: three sequence-level, five timestep-level."""

# The scored window and the rate its timestep-level targets are sampled at. Part of the
# evaluation contract, so they live here rather than on a dataset class.
BEHAVIOR_SFREQ = 50  # Hz
TARGET_WINDOW = 1.0  # seconds


[docs] @dataclass class TS1ReadoutSpec: """Specification for a single decoding readout head. Defines the target variable's type, dimensionality, data keys, and evaluation metrics for one task in the benchmark. Attributes: id: Unique identifier for the readout (typically the task name). dim: Output dimensionality of the readout head. data_type: Type of the target variable (continuous, binary, etc.). target_resolution: Whether the target is at the item or timestep level. primary_metric: Name of the metric used for model selection and early stopping. metrics: Mapping from metric name to a callable that instantiates the metric. value_key: Key in the data dict for the target values. domain_key: Key in the data dict for the domain identifier. interval_key: Key in the data dict for the trial intervals. timestamp_key: Key in the data dict for target timestamps. Optional. mask_key: Key in the data dict for the validity mask. Optional. mask_counts_key: Key in the data dict for the mask counts. Optional. """ id: str dim: int data_type: DataType target_resolution: TargetResolution # metrics primary_metric: str metrics: dict[str, Callable] # data keys value_key: str domain_key: str interval_key: str timestamp_key: str | None = None mask_key: str | None = None mask_counts_key: str | None = None @property def num_timesteps(self) -> int: """Target steps the readout emits. Derived, so it cannot drift from the resolution.""" if self.target_resolution == TargetResolution.SEQUENCE: return 1 return int(BEHAVIOR_SFREQ * TARGET_WINDOW)
[docs] def get_ts1_readout_spec(task: TS1Task) -> TS1ReadoutSpec: """What ``task`` predicts, and the metrics it is scored on.""" from torchmetrics import ( Accuracy, AveragePrecision, F1Score, MeanAbsoluteError, PearsonCorrCoef, R2Score, ) from ibl_bwb_eval.metrics import PoissonD2Score match task: # classification tasks case "choice": return TS1ReadoutSpec( id="choice", dim=2, data_type=DataType.BINARY, target_resolution=TargetResolution.SEQUENCE, value_key="task_aligned_intervals.choice.choice", interval_key="task_aligned_intervals.choice", domain_key="task_aligned_intervals.choice", primary_metric="bacc", metrics={ "bacc": partial(Accuracy, task="multiclass", num_classes=2, average="macro"), "f1": partial(F1Score, task="multiclass", num_classes=2, average="macro"), "ap": partial( AveragePrecision, task="multiclass", num_classes=2, average="macro" ), }, ) case "reward": return TS1ReadoutSpec( id="reward", dim=2, data_type=DataType.BINARY, target_resolution=TargetResolution.SEQUENCE, value_key="task_aligned_intervals.reward.reward", interval_key="task_aligned_intervals.reward", domain_key="task_aligned_intervals.reward", primary_metric="bacc", metrics={ "bacc": partial(Accuracy, task="multiclass", num_classes=2, average="macro"), "f1": partial(F1Score, task="multiclass", num_classes=2, average="macro"), "ap": partial( AveragePrecision, task="multiclass", num_classes=2, average="macro" ), }, ) case "stimulus_contrast": return TS1ReadoutSpec( id="stimulus_contrast", dim=5, data_type=DataType.MULTINOMIAL, target_resolution=TargetResolution.SEQUENCE, value_key="task_aligned_intervals.stimulus_contrast.stimulus_contrast", interval_key="task_aligned_intervals.stimulus_contrast", domain_key="task_aligned_intervals.stimulus_contrast", primary_metric="bacc", metrics={ "bacc": partial(Accuracy, task="multiclass", num_classes=5, average="macro"), "f1": partial(F1Score, task="multiclass", num_classes=5, average="macro"), "ap": partial( AveragePrecision, task="multiclass", num_classes=5, average="macro" ), }, ) # regression tasks case "whisker_motion_energy": return TS1ReadoutSpec( id="whisker_motion_energy", dim=1, data_type=DataType.CONTINUOUS, target_resolution=TargetResolution.TIMESTEP, timestamp_key="whisker.timestamps", value_key="whisker.whisker_motion_energy", interval_key="task_aligned_intervals.movement_window", domain_key="whisker._domain", primary_metric="r2", metrics={ "r2": R2Score, "mae": MeanAbsoluteError, "pearson": partial(PearsonCorrCoef, num_outputs=1), }, ) case "wheel_speed": return TS1ReadoutSpec( id="wheel_speed", dim=1, data_type=DataType.CONTINUOUS, target_resolution=TargetResolution.TIMESTEP, timestamp_key="wheel.timestamps", value_key="wheel.wheel_speed", interval_key="task_aligned_intervals.movement_window", domain_key="wheel._domain", primary_metric="r2", metrics={ "r2": R2Score, "mae": MeanAbsoluteError, "pearson": partial(PearsonCorrCoef, num_outputs=1), }, ) case "right_paw_speed": return TS1ReadoutSpec( id="right_paw_speed", dim=1, data_type=DataType.CONTINUOUS, target_resolution=TargetResolution.TIMESTEP, timestamp_key="paws.timestamps", value_key="paws.right_paw_speed", mask_key="paws.is_right_paw_confident", mask_counts_key="task_aligned_intervals.movement_window.num_right_paw_confident", interval_key="task_aligned_intervals.movement_window", domain_key="paws._domain", primary_metric="r2", metrics={ "r2": R2Score, "mae": MeanAbsoluteError, "pearson": partial(PearsonCorrCoef, num_outputs=1), }, ) case "left_paw_speed": return TS1ReadoutSpec( id="left_paw_speed", dim=1, data_type=DataType.CONTINUOUS, target_resolution=TargetResolution.TIMESTEP, timestamp_key="paws.timestamps", value_key="paws.left_paw_speed", mask_key="paws.is_left_paw_confident", mask_counts_key="task_aligned_intervals.movement_window.num_left_paw_confident", interval_key="task_aligned_intervals.movement_window", domain_key="paws._domain", primary_metric="r2", metrics={ "r2": R2Score, "mae": MeanAbsoluteError, "pearson": partial(PearsonCorrCoef, num_outputs=1), }, ) case "licking_rate": return TS1ReadoutSpec( id="licking_rate", dim=1, data_type=DataType.EVENT_RATE, target_resolution=TargetResolution.TIMESTEP, timestamp_key="licks.timestamps", value_key="licks.licking_rate", interval_key="task_aligned_intervals.licking_window", domain_key="licks._domain", primary_metric="poisson_d2", metrics={ "poisson_d2": PoissonD2Score, "mae": MeanAbsoluteError, }, ) case _: raise ValueError( f"{task!r} is not a scored TS1 task, expected one of {get_args(TS1Task)}" )
[docs] def get_ts1_supported_tasks(target_resolution: TargetResolution | None = None) -> list[TS1Task]: """Returns a list of all supported tasks. The items are TS1Task enum values, each with a string value naming the task. Returns: list[BenchmarkTasks]: A list of all supported tasks. Example: >>> tasks = get_ts1_supported_tasks() >>> print(tasks) ['choice', ...] """ if target_resolution is None: return list(get_args(TS1Task)) assert target_resolution in TargetResolution, ( f"{target_resolution} is not a valid TargetResolution" ) filtered_tasks = [ task for task in get_args(TS1Task) if get_ts1_readout_spec(task).target_resolution == target_resolution ] return filtered_tasks