Open In Colab

Installing requirements¶

The cell below installs every Python package needed to run this notebook, at fully pinned versions, using uv for fast resolution. In Colab the cell is collapsed by default — click the ▶ button to run it.

In [1]:
# install cell skipped during CI (deps preinstalled into system Python)

⚠️ Restart runtime after install

The install may upgrade packages already loaded in the kernel. Go to Runtime → Restart session, then Run all cells below (skip this install cell on re-run).

IBL Widefield — Raw Data Tutorial¶

This tutorial shows how to access data from DANDI:001712 for the IBL widefield dataset.

Study Overview¶

This dataset contains widefield imaging data from mice, investigating how prior expectations and neural dynamics are modified in a mouse model of autism (CNTNALP2 knockout). The study explores sensory perception and decision-making tasks to understand the neural basis of altered prior utilization.

Contents¶

  1. Setup and Data Access
  2. Session and Subject Metadata
  3. Raw Imaging Data and Metadata
    • OnePhotonSeries
    • Imaging Metadata
  4. Synchronization Signals
    • Digital Signals
    • Analog Signals
  5. Behavior
    • Epochs (Task vs Passive)
    • Raw Video Data

1. Setup and Data Access ¶

Import Required Libraries¶

In [2]:
# Visualization
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np

# Configure matplotlib
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 10

2. Session and Subject Metadata ¶

In [3]:
from load_nwb_utils import *

dandiset_id = "001712"
subject_id = "FD-28"  # Example subject
session_id = "81f90b18-e61c-4d32-bbce-3e0c5f33f06c"  # EID for the session

# Choose data source (DANDI streaming or local)
USE_DANDI = True  # Set to False to use local files

if USE_DANDI:
    nwbfile, io = load_nwb_from_dandi(dandiset_id, subject_id, session_id, description="raw")
else:
    # TODO Specify your local directory path
    local_directory = f"E:/IBL-widefield-nwbfiles/full/"
    nwbfile, io = load_nwb_local(local_directory, subject_id, session_id, description="raw")

print("=== SESSION INFORMATION ===")
print(f"Experiment description:\n {nwbfile.experiment_description}")
print(f"Session description:\n {nwbfile.session_description}")
print(f"Session start time:\n {nwbfile.session_start_time}")
=== SESSION INFORMATION ===
Experiment description:
 In dynamic environments, updating beliefs based on past experiences (priors) is essential for optimal decision-making. Prior utilization is often impaired in psychiatric disorders, affecting perception and behavior. We investigate how Neurexin1α (Nrxn1α) loss-of-function disrupts this process, providing insight into circuit deficits underlying sensorimotor dysfunction. While the synaptic role of Nrxn1α is well studied, its impact on network dynamics and decision-making behavior remain unclear. Using widefield calcium imaging, we assess cortex-wide activity in mice performing a two-choice task to probe how priors influence visually-guided decisions. This task requires the mouse to combine sensory evidence with the prior probability over the stimulus side. We find Nrxn1α KO mice underutilized priors and were slower to update choices based on feedback. During decision-making, cortex-wide cortical activity is both elevated and increasingly correlated in Nrxn1α KO mice, independent of task period. Moreover, a larger fraction of cortical variance was explained by movement variables, consistent with stronger coupling of cortical activity to motor signals and a bias toward movement-related dynamics. These findings suggest that core computations underlying decision-making, such as integrating past experience with current evidence, depend on intact synaptic mechanisms shaped by genes like Nrxn1α.

Session description:
 The task protocol(s) performed in this experimental session:
1. Biased choice world — the standard IBL data-collection task for trained mice. A Gabor patch appears at ±35° azimuth and the mouse turns a wheel to bring it to the center. Correct responses earn a water reward (~1.5 µL); incorrect responses trigger white noise and a 2s timeout. Stimulus probability alternates between 80/20 and 20/80 blocks (starting with a 50/50 block), with block lengths drawn from a truncated exponential distribution (min 20, max 100 trials). Full contrast set: [1.0, 0.25, 0.125, 0.0625, 0.0]. 

Session start time:
 2023-11-14 12:10:33.757227-08:00
In [4]:
print("\n=== SUBJECT INFORMATION ===")
print(f"ID: {nwbfile.subject.subject_id}")
print(f"DOB: {nwbfile.subject.date_of_birth}")
print(f"Strain: {nwbfile.subject.species}")
print(f"Genotype: {nwbfile.subject.genotype}")
print(f"Sex: {nwbfile.subject.sex}")
=== SUBJECT INFORMATION ===
ID: FD-28
DOB: 2022-12-08 00:00:00-08:00
Strain: Mus musculus
Genotype: None
Sex: F

3. Raw Imaging Data and Metadata ¶

OnePhotonSeries ¶

Raw widefield imaging data are stored as OnePhotonSeries objects in the NWB file's acquisition module. Each excitation wavelength is added to a separate OnePhotonSeries object.

Data shape

  • Arrays are shaped (time, num_rows, num_columns) — the first dimension is time (frame), the second and third are the height and width of the image.

Series names

  • Calcium (470 nm): OnePhotonSeriesCalcium
  • Isosbestic (405 nm): OnePhotonSeriesIsosbestic

Access example

calcium = nwbfile.acquisition['OnePhotonSeriesCalcium']
first_frame = calcium.data[0]  # lazy read of frame 0
In [5]:
calcium_imaging = nwbfile.acquisition["OnePhotonSeriesCalcium"]

print("=== RAW WIDEFIELD CALCIUM SIGNAL ===")
print(f"Name: {calcium_imaging.name}")
print(f"Description: {calcium_imaging.description}")
print(f"Data shape: {calcium_imaging.data.shape}")
print(f"Duration: {calcium_imaging.timestamps[-1] - calcium_imaging.timestamps[0]:.2f} seconds")
=== RAW WIDEFIELD CALCIUM SIGNAL ===
Name: OnePhotonSeriesCalcium
Description: Widefield raw imaging under blue excitation at 470 nm (GCaMP signal). The dimensions are (time, height, width).
Data shape: (124886, 540, 640)
Duration: 3999.27 seconds
In [6]:
isosbestic_imaging = nwbfile.acquisition["OnePhotonSeriesIsosbestic"]

print("=== RAW WIDEFIELD ISOSBESTIC SIGNAL ===")
print(f"Name: {isosbestic_imaging.name}")
print(f"Description: {isosbestic_imaging.description}")
print(f"Data shape: {isosbestic_imaging.data.shape}")
print(f"Duration: {isosbestic_imaging.timestamps[-1] - isosbestic_imaging.timestamps[0]:.2f} seconds")
=== RAW WIDEFIELD ISOSBESTIC SIGNAL ===
Name: OnePhotonSeriesIsosbestic
Description: Widefield raw imaging under violet excitation at 405 nm (isosbestic control). The dimensions are (time, height, width).
Data shape: (124886, 540, 640)
Duration: 3999.27 seconds
In [7]:
# Plot with matplotlib (example)

fig, axes = plt.subplots(1, 2, sharex=True, sharey=True, dpi=100)
axes[0].imshow(calcium_imaging.data[0, ...], cmap='gray')
axes[0].set_title('Calcium Imaging (470 nm) - First Frame')
axes[0].axis('off')
axes[1].imshow(isosbestic_imaging.data[0, ...], cmap='gray')
axes[1].set_title('Isosbestic Imaging (405 nm) - First Frame')
axes[1].axis('off')
plt.tight_layout()
plt.show()
No description has been provided for this image

Imaging Metadata ¶

In [8]:
print("=== IMAGING METADATA ===")
print("All imaging metadata are stored in the imaging module in imaging_planes:")
print("-" * 100)
for imaging_plane_name in nwbfile.imaging_planes:
    imaging_plane = nwbfile.imaging_planes[imaging_plane_name]
    print(f"Imaging Plane: {imaging_plane_name}")
    print("-" * 100)
    print(f"  Description: {imaging_plane.description}")
    print(f"  Imaging Rate: {imaging_plane.imaging_rate} Hz")
    print(f"  Optical Channel: {imaging_plane.optical_channel[0].name}, {imaging_plane.optical_channel[0].description}")
    print(f"  Indicator: {imaging_plane.indicator}")
    print(f"  Excitation Wavelength: {imaging_plane.excitation_lambda} nm")
    print(f"  Emission Wavelength: {imaging_plane.optical_channel[0].emission_lambda} nm")
    print(f"  Location: {imaging_plane.location}")
    print("-"*100)
=== IMAGING METADATA ===
All imaging metadata are stored in the imaging module in imaging_planes:
----------------------------------------------------------------------------------------------------
Imaging Plane: ImagingPlaneCalcium
----------------------------------------------------------------------------------------------------
  Description: The imaging plane for calcium imaging from Blue light excitation.
  Imaging Rate: 15.0 Hz
  Optical Channel: OpticalChannel, GCaMP Ca2+ bound emission (calcium signal). (Thorlabs, cat. no. M470L4)
  Indicator: GCaMP6f
  Excitation Wavelength: 470.0 nm
  Emission Wavelength: 510.0 nm
  Location: Whole Brain
----------------------------------------------------------------------------------------------------
Imaging Plane: ImagingPlaneIsosbestic
----------------------------------------------------------------------------------------------------
  Description: The imaging plane for calcium imaging from Violet light excitation.
  Imaging Rate: 15.0 Hz
  Optical Channel: OpticalChannel, GCaMP Ca2+ independent emission (isosbestic signal). (Thorlabs, cat. no. M405L4)
  Indicator: GCaMP6f
  Excitation Wavelength: 405.0 nm
  Emission Wavelength: 510.0 nm
  Location: Whole Brain
----------------------------------------------------------------------------------------------------

4. Synchronization Signals ¶

Digital Signals ¶

In [9]:
event_names = [name for name in nwbfile.acquisition if "Events" in name]
print("=== SYNCHRONIZATION EVENTS IN ACQUISITION ===")
print("-" * 100)
for name in event_names:
    print(f"  Name: {name}")
    print(f"  Description: {nwbfile.acquisition[name].description}")
=== SYNCHRONIZATION EVENTS IN ACQUISITION ===
----------------------------------------------------------------------------------------------------
  Name: EventsAudio
  Description: Auditory stimulus presentation events. Marks timing of audio stimulus delivery for auditory tasks or cue presentation.

Label meanings:
  - audio_off: Audio stimulus off
  - audio_on: Audio stimulus on
  Name: EventsBodyCamera
  Description: Video frame acquisition times for the body camera. Each event marks when a video frame was captured by the camera, enabling temporal alignment of behavior videos with neural and task data.

Label meanings:
  - exposure_end: Camera exposure end or frame readout complete
  - frame_start: Camera frame acquisition start (frame timestamp)
  Name: EventsFrame2ttl
  Description: Monitor refresh events detected by photodiode for visual stimulus timing. The Frame2TTL device uses a photodiode to detect changes in screen luminance, providing precise timing of when visual stimuli are displayed on the monitor. This is essential for accurate stimulus-response latency measurements.

Label meanings:
  - screen_dark: Screen transitioned to dark (photodiode detected low luminance)
  - screen_bright: Screen transitioned to bright (photodiode detected high luminance)
  Name: EventsFrameTrigger
  Description: Widefield imaging frame trigger events. Each event marks the trigger signal that initiates acquisition of a widefield imaging frame, providing precise timing for aligning imaging data with behavioral and electrophysiological recordings.

Label meanings:
  - frame_off: Frame trigger signal off
  - frame_on: Frame trigger signal on
  Name: EventsLeftCamera
  Description: Video frame acquisition times for the left-side camera. Each event marks when a video frame was captured by the camera, enabling temporal alignment of behavior videos with neural and task data.

Label meanings:
  - exposure_end: Camera exposure end or frame readout complete
  - frame_start: Camera frame acquisition start (frame timestamp)
  Name: EventsRightCamera
  Description: Video frame acquisition times for the right-side camera. Each event marks when a video frame was captured by the camera, enabling temporal alignment of behavior videos with neural and task data.

Label meanings:
  - exposure_end: Camera exposure end or frame readout complete
  - frame_start: Camera frame acquisition start (frame timestamp)
  Name: EventsRotaryEncoder0
  Description: Rotary encoder pulses tracking wheel movement (quadrature phase A). Each pulse represents a discrete angular increment of the wheel rotation, used to measure behavioral responses and locomotion with high temporal precision.

Label meanings:
  - phase_low: Encoder phase transition to LOW
  - phase_high: Encoder phase transition to HIGH
  Name: EventsRotaryEncoder1
  Description: Rotary encoder pulses tracking wheel movement (quadrature phase B). Combined with rotary_encoder_0, this provides directional information for wheel rotation through quadrature encoding.

Label meanings:
  - phase_low: Encoder phase transition to LOW
  - phase_high: Encoder phase transition to HIGH
In [10]:
from itertools import cycle


def squares(tscale, polarity, ax=None, yrange=[0, 1], **kwargs):
    """
    Plot rising/falling fronts as a square wave.
    """
    if ax is None:
        ax = plt.gca()
    isort = np.argsort(tscale)
    tscale = tscale[isort]
    polarity = polarity[isort]
    f = np.tile(polarity, (2, 1))
    t = np.concatenate((tscale, np.r_[tscale[1:], tscale[-1]])).reshape(2, f.shape[1])
    ydata = f.T.ravel()
    ydata = (ydata + 1) / 2 * (yrange[1] - yrange[0]) + yrange[0]
    ax.plot(t.T.ravel(), ydata, **kwargs)

num_events = 11  # number of on-off events to plot
cmap = plt.get_cmap('tab20')
color_cycle = cycle([cmap(i) for i in range(cmap.N)])

for i, event_name in enumerate(event_names):
    fig, ax = plt.subplots(figsize=(8, 2), dpi=200)
    event = nwbfile.acquisition[event_name]

    # take first num_events on/off pairs (2 * num_events samples)
    times = event.timestamps[:num_events * 2]
    data = event.data[:num_events * 2].astype(int)

    # find change points (edges)
    changes = np.where(np.diff(data) != 0)[0] + 1
    if changes.size == 0:
        continue

    tscale = times[changes]
    prev_vals = data[changes - 1]
    next_vals = data[changes]

    # +1 for 0->1 (rising), -1 for 1->0 (falling)
    polarity = np.where(
        (prev_vals == 0) & (next_vals == 1), 1,
        np.where((prev_vals == 1) & (next_vals == 0), -1, 0)
    )
    valid = polarity != 0
    tscale = tscale[valid]
    polarity = polarity[valid]

    color = next(color_cycle)

    # plot as square wave
    squares(tscale, polarity, ax=ax, yrange=[0, 1], color=color, linewidth=1.5)

    # if i == 0:
    #     axes[i].set_title("Synchronization Events (acquisition)")

    legend_patches = [mpatches.Patch(color=color, alpha=0.8, label=event_name)]
    ax.legend(handles=legend_patches, bbox_to_anchor=(1.01, 1), loc='upper left')

    ax.set_yticks([0, 1])
    ax.set_yticklabels(['0', '1'])
    ax.set_frame_on(False)

    if i == len(event_names) - 1:
        ax.set_xlabel("Time (s)")
    # turn off grid
    #ax.grid(False)

plt.tight_layout()
plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Analog Signals ¶

In [11]:
analog_signal_names = [name for name in nwbfile.acquisition if "TimeSeries" in name]
print("=== SYNCHRONIZATION SIGNALS IN ACQUISITION ===")
print("-" * 100)
for name in analog_signal_names:
    print(f"  Name: {name}")
    print(f"  Description: {nwbfile.acquisition[name].description}")
=== SYNCHRONIZATION SIGNALS IN ACQUISITION ===
----------------------------------------------------------------------------------------------------
  Name: TimeSeriesBpod
  Description: Analog signal from Bpod behavioral control system. This continuous voltage signal encodes behavioral state machine events and timestamps from the Bpod system, which controls stimulus presentation and reward delivery during tasks. The analog encoding allows precise temporal alignment between Bpod events and neural recordings.
In [12]:
signal_name = "TimeSeriesBpod"
analog_signal = nwbfile.acquisition[signal_name]
rate = analog_signal.rate

# desired time window (minutes)
start_min, end_min = 5, 6
start_sec, end_sec = start_min * 60, end_min * 60

# convert absolute seconds to sample indices (respecting starting_time)
start_idx = int((start_sec - analog_signal.starting_time) * rate)
end_idx = int((end_sec - analog_signal.starting_time) * rate)

# clamp to valid range
start_idx = max(0, start_idx)
end_idx = min(len(analog_signal.data), end_idx)

data_slice = analog_signal.data[start_idx:end_idx] * analog_signal.conversion
times = analog_signal.starting_time + np.arange(start_idx, end_idx) / rate

fig, ax = plt.subplots(dpi=200)
ax.plot(times, data_slice, label=signal_name)
ax.set_xlim(start_sec, end_sec)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Voltage (V)")
ax.set_title("Analog Synchronization Signal")
ax.legend(bbox_to_anchor=(1.01, 1), loc='upper left')
ax.set_frame_on(False)
plt.show()
No description has been provided for this image

5. Behavior ¶

Epochs (Task vs Passive) ¶

In [13]:
nwbfile.epochs

Raw Video Data ¶

In [14]:
print("=== RAW VIDEO ===\n")
image_series = []
for name, acq in nwbfile.acquisition.items():
    if "Video" not in name:
        continue
    print(f"{name} - {acq.description}: ")
    print(f"  External file: {acq.external_file[:]}")
    print("-" * 100)
    image_series.append(name)
=== RAW VIDEO ===

VideoBodyCamera - Raw video from camera recording behavioral and task events.: 
  External file: ['./sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_ecephys+image\\sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_VideoBodyCamera.mp4']
----------------------------------------------------------------------------------------------------
VideoLeftCamera - Raw video from camera recording behavioral and task events.: 
  External file: ['./sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_ecephys+image\\sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_VideoLeftCamera.mp4']
----------------------------------------------------------------------------------------------------
VideoRightCamera - Raw video from camera recording behavioral and task events.: 
  External file: ['./sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_ecephys+image\\sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_VideoRightCamera.mp4']
----------------------------------------------------------------------------------------------------
In [15]:
nwbfile.acquisition["VideoBodyCamera"]
Out[15]:

VideoBodyCamera (ImageSeries)

resolution: -1.0
comments: no comments
description: Raw video from camera recording behavioral and task events.
conversion: 1.0
offset: 0.0
unit: n.a.
data
HDF5 dataset
Data typeuint8
Shape(0, 0, 0)
Array size0.00 bytes
Chunk shapeNone
CompressionNone
Compression optsNone
Uncompressed size (bytes)0
Compressed size (bytes)0
Compression ratioundefined

[]
timestamps
HDF5 dataset
Data typefloat64
Shape(119964,)
Array size937.22 KiB
Chunk shape(119964,)
Compressiongzip
Compression opts4
Uncompressed size (bytes)959712
Compressed size (bytes)482800
Compression ratio1.987804473902237
timestamps_unit: seconds
interval: 1
external_file
HDF5 dataset
Data typeobject
Shape(1,)
Array size8.00 bytes
Chunk shapeNone
CompressionNone
Compression optsNone
Uncompressed size (bytes)8
Compressed size (bytes)16
Compression ratio0.5

['./sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_ecephys+image\\sub-FD-28_ses-81f90b18-e61c-4d32-bbce-3e0c5f33f06c_VideoBodyCamera.mp4']
starting_frame
NumPy array
Data typeint64
Shape(1,)
Array size8.00 bytes

[0]
format: external
In [ ]: