Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add week long session #18

Merged
merged 6 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cai_lab_to_nwb/zaki_2024/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .eztrack_interface import EzTrackFreezingBehaviorInterface
from .zaki_2024_edf_interface import Zaki2024EDFInterface
from .zaki_2024_edf_interface import Zaki2024EDFInterface, Zaki2024MultiEDFInterface
from .minian_interface import MinianSegmentationInterface, MinianMotionCorrectionInterface
from .zaki_2024_sleep_classification_interface import Zaki2024SleepClassificationInterface
from .miniscope_imaging_interface import MiniscopeImagingInterface
Expand Down
104 changes: 102 additions & 2 deletions src/cai_lab_to_nwb/zaki_2024/interfaces/zaki_2024_edf_interface.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from typing import Union
from pathlib import Path
from neuroconv.basedatainterface import BaseDataInterface

from pynwb import NWBFile, TimeSeries
from pynwb.device import Device

from neuroconv.basedatainterface import BaseDataInterface
from neuroconv.utils import DeepDict

from mne.io import read_raw_edf
from datetime import datetime, timedelta
import numpy as np
Expand All @@ -10,7 +15,7 @@
class Zaki2024EDFInterface(BaseDataInterface):
def __init__(
self,
file_path: Path,
file_path: Union[Path, str],
verbose: bool = False,
):
self.file_path = Path(file_path)
Expand Down Expand Up @@ -117,3 +122,98 @@ def add_to_nwbfile(
nwbfile.add_device(device)

return nwbfile


class Zaki2024MultiEDFInterface(BaseDataInterface):
def __init__(
self,
file_paths: list[Path],
verbose: bool = False,
):
self.file_paths = file_paths
self.verbose = verbose
super().__init__(file_paths=file_paths)

def add_to_nwbfile(
self,
nwbfile: NWBFile,
stub_test: bool = False,
stub_frames: int = 100,
**conversion_options,
) -> NWBFile:
"""
Adds data from EEG, EMG, temperature, and activity channels to an NWBFile.

Parameters
----------
nwbfile : NWBFile
The NWBFile object to which data will be added.
stub_test : bool, optional
If True, loads only a subset of frames (controlled by `stub_frames` parameter)
to facilitate testing and faster execution. Default is False.
stub_frames : int, optional
The number of frames to load if `stub_test` is True. Default is 100.

Returns
-------
NWBFile
The NWBFile object with added data and metadata from the specified channels.
"""
channels_dict = {
"Temp": {
"name": "TemperatureSignal",
"description": "Temperature signal recorder with HD-X02 wireless telemetry probe",
"unit": "celsius",
},
"EEG": {
"name": "EEGSignal",
"description": "EEG signal recorder with HD-X02 wireless telemetry probe",
"unit": "volts",
},
"EMG": {
"name": "EMGSignal",
"description": "EMG signal recorder with HD-X02 wireless telemetry probe",
"unit": "volts",
},
# TODO: Figure out if the units of activity are correct, the raw format marks Volts
# TODO: My own reading of the header indicates that the physical units is counts
"Activity": {
"name": "ActivitySignal",
"description": "Activity signal recorder with HD-X02 wireless telemetry probe. It refers to the motion of the probe relative to the receiver and it can be used as a proxy for locomotion.",
"unit": "n.a.",
},
}
concatenated_data = None
concatenated_times = []

# Loop over each EDF file and concatenate data and timestamps
for file_path in self.file_paths:
edf_reader = read_raw_edf(input_fname=file_path, verbose=self.verbose)
data, times = edf_reader.get_data(picks=list(channels_dict.keys()), return_times=True)
data = data.astype(np.float32)
# Slice the data and timestamps within the time range
if stub_test:
concatenated_data = data[:, :stub_frames]
break
# Concatenate along the time axis
if concatenated_data is None:
concatenated_data = data
else:
concatenated_data = np.concatenate((concatenated_data, data), axis=1)
concatenated_times.extend(times)

for channel_index, channel_name in enumerate(channels_dict.keys()):
time_series_kwargs = channels_dict[channel_name].copy()
time_series_kwargs.update(
data=concatenated_data[channel_index], starting_time=0.0, rate=edf_reader.info["sfreq"]
)
time_series = TimeSeries(**time_series_kwargs)
nwbfile.add_acquisition(time_series)

# Add device
description = "Wireless telemetry probe used to record EEG, EMG, temperature, and activity data"
name = "HD-X02, Data Science International"
device = Device(name=name, description=description)
nwbfile.add_device(device)

return nwbfile
98 changes: 98 additions & 0 deletions src/cai_lab_to_nwb/zaki_2024/zaki_2024_convert_week_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Primary script to run to convert an entire session for of data using the NWBConverter."""

import time
from natsort import natsorted
from pathlib import Path
from typing import Union
from datetime import datetime, timedelta
import pandas as pd
import json
from neuroconv.utils import load_dict_from_file, dict_deep_update

from zaki_2024_nwbconverter import Zaki2024NWBConverter


def session_to_nwb(
data_dir_path: Union[str, Path],
output_dir_path: Union[str, Path],
subject_id: str,
stub_test: bool = False,
verbose: bool = True,
):

if verbose:
print(f"Converting week-long session")
start = time.time()

data_dir_path = Path(data_dir_path)
output_dir_path = Path(output_dir_path)
if stub_test:
output_dir_path = output_dir_path / "nwb_stub"
output_dir_path.mkdir(parents=True, exist_ok=True)

nwbfile_path = output_dir_path / f"{subject_id}_week_session.nwb"

source_data = dict()
conversion_options = dict()

# Add EEG, EMG, Temperature and Activity signals
edf_folder_path = data_dir_path / "Ca_EEG_EDF" / (subject_id + "_EDF")
edf_file_paths = natsorted(edf_folder_path.glob("*.edf"))
assert edf_file_paths, f"No .edf files found in {edf_folder_path}"

source_data.update(
dict(
MultiEDFSignals=dict(
file_paths=edf_file_paths,
)
)
)
conversion_options.update(dict(MultiEDFSignals=dict(stub_test=stub_test)))

converter = Zaki2024NWBConverter(source_data=source_data)

# Add datetime to conversion
metadata = converter.get_metadata()

from mne.io import read_raw_edf

edf_reader = read_raw_edf(input_fname=edf_file_paths[0], verbose=verbose)
session_start_time = edf_reader.info["meas_date"]
metadata["NWBFile"]["session_start_time"] = session_start_time

# Update default metadata with the editable in the corresponding yaml file
editable_metadata_path = Path(__file__).parent / "zaki_2024_metadata.yaml"
editable_metadata = load_dict_from_file(editable_metadata_path)
metadata = dict_deep_update(metadata, editable_metadata)

metadata["Subject"]["subject_id"] = subject_id

# Run conversion
converter.run_conversion(
metadata=metadata, nwbfile_path=nwbfile_path, conversion_options=conversion_options, overwrite=True
)

if verbose:
stop_time = time.time()
conversion_time_seconds = stop_time - start
if conversion_time_seconds <= 60 * 3:
print(f"Conversion took {conversion_time_seconds:.2f} seconds")
elif conversion_time_seconds <= 60 * 60:
print(f"Conversion took {conversion_time_seconds / 60:.2f} minutes")
else:
print(f"Conversion took {conversion_time_seconds / 60 / 60:.2f} hours")


if __name__ == "__main__":

# Parameters for conversion
data_dir_path = Path("D:/")
output_dir_path = Path("D:/cai_lab_conversion_nwb/")
subject_id = "Ca_EEG3-4"
stub_test = False
session_to_nwb(
data_dir_path=data_dir_path,
output_dir_path=output_dir_path,
stub_test=stub_test,
subject_id=subject_id,
)
2 changes: 2 additions & 0 deletions src/cai_lab_to_nwb/zaki_2024/zaki_2024_nwbconverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from interfaces import (
MinianSegmentationInterface,
Zaki2024EDFInterface,
Zaki2024MultiEDFInterface,
EzTrackFreezingBehaviorInterface,
Zaki2024SleepClassificationInterface,
MiniscopeImagingInterface,
Expand All @@ -27,6 +28,7 @@ class Zaki2024NWBConverter(NWBConverter):
MinianMotionCorrection=MinianMotionCorrectionInterface,
SleepClassification=Zaki2024SleepClassificationInterface,
EDFSignals=Zaki2024EDFInterface,
MultiEDFSignals=Zaki2024MultiEDFInterface,
FreezingBehavior=EzTrackFreezingBehaviorInterface,
Video=VideoInterface,
ShockStimuli=Zaki2024ShockStimuliInterface,
Expand Down