diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/__init__.py new file mode 100644 index 0000000..aa4e25b --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/__init__.py @@ -0,0 +1 @@ +from .fiber_photometry_nwbconverter import FiberPhotometryNWBConverter diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/all_sessions_table.xlsx b/src/constantinople_lab_to_nwb/fiber_photometry/all_sessions_table.xlsx new file mode 100644 index 0000000..c98aed1 Binary files /dev/null and b/src/constantinople_lab_to_nwb/fiber_photometry/all_sessions_table.xlsx differ diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/convert_all_sessions.py b/src/constantinople_lab_to_nwb/fiber_photometry/convert_all_sessions.py new file mode 100644 index 0000000..a12a54a --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/convert_all_sessions.py @@ -0,0 +1,330 @@ +"""Primary script to run to convert all sessions in a dataset using session_to_nwb.""" + +import os +import traceback +from concurrent.futures import ( + ProcessPoolExecutor, + as_completed, +) +from copy import deepcopy +from datetime import datetime +from pathlib import Path +from pprint import pformat +from typing import Union + +import numpy as np +import pandas as pd +from neuroconv.utils import load_dict_from_file +from tqdm import tqdm + +from constantinople_lab_to_nwb.fiber_photometry.fiber_photometry_convert_session import session_to_nwb + +from constantinople_lab_to_nwb.utils import get_subject_metadata_from_rat_info_folder + + +def update_default_fiber_photometry_metadata( + session_data: pd.DataFrame, +): + + session_data = session_data.reset_index(drop=True) + raw_fiber_photometry_file_path = session_data["raw_fiber_photometry_file_path"].values[0] + raw_fiber_photometry_file_path = Path(raw_fiber_photometry_file_path) + + if raw_fiber_photometry_file_path.suffix == ".csv": + default_fiber_photometry_metadata_yaml_file_path = ( + Path(__file__).parent / "metadata" / "doric_csv_fiber_photometry_metadata.yaml" + ) + elif raw_fiber_photometry_file_path.suffix == ".doric": + default_fiber_photometry_metadata_yaml_file_path = ( + Path(__file__).parent / "metadata" / "doric_fiber_photometry_metadata.yaml" + ) + + session_data["fiber_photometry_table_region"] = session_data.groupby( + ["emission_wavelength_nm", "excitation_wavelength_nm"] + ).ngroup() + session_data.sort_values(by=["fiber_photometry_table_region"], inplace=True) + + # For debugging print the DataFrame + # pd.set_option('display.max_rows', None) # Show all rows + # pd.set_option('display.max_columns', None) # Show all columns + # pd.set_option('display.width', 1000) # Set display width to avoid wrapping + # pd.set_option('display.max_colwidth', None) # Show full column content + # + # # Your code to create and print the DataFrame + # print(session_data[["emission_wavelength_nm", "excitation_wavelength_nm", "fiber_photometry_table_region"]]) + + default_fiber_photometry_metadata = load_dict_from_file(file_path=default_fiber_photometry_metadata_yaml_file_path) + fiber_photometry_metadata_copy = deepcopy(default_fiber_photometry_metadata) + + series_metadata = fiber_photometry_metadata_copy["Ophys"]["FiberPhotometry"]["FiberPhotometryResponseSeries"] + default_fiber_photometry_table_metadata = fiber_photometry_metadata_copy["Ophys"]["FiberPhotometry"][ + "FiberPhotometryTable" + ] + + indicators_metadata = fiber_photometry_metadata_copy["Ophys"]["FiberPhotometry"]["Indicators"] + excitation_sources_metadata = fiber_photometry_metadata_copy["Ophys"]["FiberPhotometry"]["ExcitationSources"] + + default_rows_in_fiber_photometry_table = default_fiber_photometry_table_metadata["rows"] + + fiber_photometry_table_rows = [] + indicators_to_add = [] + excitation_sources_to_add = [] + for region, region_data in session_data.groupby("fiber_photometry_table_region"): + row_metadata = next( + (row for row in default_rows_in_fiber_photometry_table if row["name"] == region), + None, + ) + if row_metadata is None: + raise ValueError( + f"FiberPhotometryTable metadata for row name '{region}' not found in '{default_fiber_photometry_metadata_yaml_file_path}'." + ) + # if any(~region_data[["fiber_position_AP", "fiber_position_ML", "fiber_position_DV"]].isna().values[0]): + # row_metadata.update(coordinates=region_data[["fiber_position_AP", "fiber_position_ML", "fiber_position_DV"]].values[0]) + # + coordinates = region_data[["fiber_position_AP", "fiber_position_ML", "fiber_position_DV"]].values[0] + + indicator_label = region_data["indicator_label"].values[0] + indicator_label = indicator_label.replace("_", "-") + indicator_metadata = next( + (indicator for indicator in indicators_metadata if indicator["label"] == indicator_label), + None, + ) + if indicator_metadata is None: + raise ValueError( + f"Indicator metadata for '{indicator_label}' not found in '{default_fiber_photometry_metadata_yaml_file_path}'." + ) + indicators_to_add.append(indicator_metadata) + + excitation_wavelength_nm = region_data["excitation_wavelength_nm"].values[0] + if np.isnan(excitation_wavelength_nm): + raise ValueError( + f"Excitation wavelength in nm is missing for indicator '{indicator_label}'. Please provide it in the xlsx file." + ) + + indicator_name = indicator_metadata["name"].lower() + excitation_source_metadata = next( + ( + source + for source in excitation_sources_metadata + if indicator_name in source["name"] + and source["excitation_wavelength_in_nm"] == float(excitation_wavelength_nm) + ), + None, + ) + if excitation_source_metadata is None: + raise ValueError( + f"Excitation source metadata for excitation wavelength '{excitation_wavelength_nm}' and indicator {indicator_name} not found in '{default_fiber_photometry_metadata_yaml_file_path}'." + f"Please provide it in the yaml file." + ) + excitation_sources_to_add.append(excitation_source_metadata) + + row_metadata.update( + location=region_data["fiber_location"].values[0], + coordinates=coordinates, + indicator=indicator_name, + excitation_source=excitation_source_metadata["name"], + ) + fiber_photometry_table_rows.append(row_metadata) + + fiber_photometry_series_metadata = [] + for series_name, series_data in session_data.groupby("fiber_photometry_series_name"): + series_metadata_to_update = next( + (series for series in series_metadata if series["name"] == series_name), + None, + ) + if series_metadata_to_update is None: + raise ValueError( + f"Series metadata for '{series_name}' not found in '{default_fiber_photometry_metadata_yaml_file_path}'." + ) + + fiber_photometry_table_region = series_data["fiber_photometry_table_region"].values + series_metadata_to_update.update(fiber_photometry_table_region=fiber_photometry_table_region) + + if "channel_column_names" in series_metadata_to_update: + series_metadata_to_update.update(channel_column_names=series_data["doric_csv_column_name"].values) + + elif "stream_names" in series_metadata_to_update: + series_metadata_to_update.update(stream_names=series_data["doric_stream_name"].values) + else: + raise ValueError( + "Either 'channel_column_names' or 'stream_names' should be present in the series metadata." + ) + fiber_photometry_series_metadata.append(series_metadata_to_update) + + fiber_photometry_metadata_copy["Ophys"]["FiberPhotometry"].update( + FiberPhotometryResponseSeries=fiber_photometry_series_metadata, + Indicators=indicators_to_add, + ExcitationSources=excitation_sources_to_add, + ) + fiber_photometry_metadata_copy["Ophys"]["FiberPhotometry"]["FiberPhotometryTable"].update( + rows=fiber_photometry_table_rows, + ) + + return fiber_photometry_metadata_copy + + +def dataset_to_nwb( + dataset_excel_file_path: Union[str, Path], + output_folder_path: Union[str, Path], + rat_info_folder_path: Union[str, Path], + max_workers: int = 1, + overwrite: bool = False, + verbose: bool = True, +): + """Convert the entire dataset to NWB. + + Parameters + ---------- + dataset_excel_file_path : Union[str, Path] + The path to the Excel file containing the dataset information. + output_folder_path : Union[str, Path] + The path to the directory where the NWB files will be saved. + rat_info_folder_path : Union[str, Path] + The path to the directory containing the rat info files. + max_workers : int, optional + The number of workers to use for parallel processing, by default 1 + overwrite : bool, optional + Whether to overwrite the NWB file if it already exists, by default False. + verbose : bool, optional + Whether to print verbose output, by default True + """ + dataset_excel_file_path = Path(dataset_excel_file_path) + os.makedirs(output_folder_path, exist_ok=True) + + session_to_nwb_kwargs_per_session = [ + session_to_nwb_kwargs + for session_to_nwb_kwargs in get_session_to_nwb_kwargs_per_session( + dataset_excel_file_path=dataset_excel_file_path, + rat_info_folder_path=rat_info_folder_path, + ) + ] + + futures = [] + with ProcessPoolExecutor(max_workers=max_workers) as executor: + for session_to_nwb_kwargs in session_to_nwb_kwargs_per_session: + session_to_nwb_kwargs["verbose"] = verbose + session_to_nwb_kwargs["overwrite"] = overwrite + nwbfile_name = Path(session_to_nwb_kwargs["nwbfile_path"]).stem + exception_file_path = ( + dataset_excel_file_path.parent / f"ERROR_{nwbfile_name}.txt" + ) # Add error file path here + futures.append( + executor.submit( + safe_session_to_nwb, + session_to_nwb_kwargs=session_to_nwb_kwargs, + exception_file_path=exception_file_path, + ) + ) + for _ in tqdm( + as_completed(futures), + total=len(futures), + ): + pass + + +def safe_session_to_nwb( + *, + session_to_nwb_kwargs: dict, + exception_file_path: Union[Path, str], +): + """Convert a session to NWB while handling any errors by recording error messages to the exception_file_path. + + Parameters + ---------- + session_to_nwb_kwargs : dict + The arguments for session_to_nwb. + exception_file_path : Path + The path to the file where the exception messages will be saved. + """ + exception_file_path = Path(exception_file_path) + try: + session_to_nwb(**session_to_nwb_kwargs) + except Exception as e: + with open( + exception_file_path, + mode="w", + ) as f: + f.write(f"session_to_nwb_kwargs: \n {pformat(session_to_nwb_kwargs)}\n\n") + f.write(traceback.format_exc()) + + +def get_session_to_nwb_kwargs_per_session( + *, + dataset_excel_file_path: Union[str, Path], + rat_info_folder_path: Union[str, Path], +): + """Get the kwargs for session_to_nwb for each session in the dataset. + + Parameters + ---------- + dataset_excel_file_path : Union[str, Path] + The path to the directory containing the raw data. + rat_info_folder_path : Union[str, Path] + The path to the directory containing the rat info files. + + Returns + ------- + list[dict[str, Any]] + A list of dictionaries containing the kwargs for session_to_nwb for each session. + """ + + dataset = pd.read_excel(dataset_excel_file_path) + + dataset_grouped = dataset.groupby(["subject_id", "session_id"]) + for (subject_id, session_id), session_data in dataset_grouped: + raw_fiber_photometry_file_path = session_data["raw_fiber_photometry_file_path"].values[0] + raw_fiber_photometry_file_path = Path(raw_fiber_photometry_file_path) + if not raw_fiber_photometry_file_path.exists(): + raise FileNotFoundError(f"File '{raw_fiber_photometry_file_path}' not found.") + + nwbfile_path = output_folder_path / f"sub-{subject_id}_ses-{session_id}.nwb" + dlc_file_path = session_data["dlc_file_path"].values[0] + video_file_path = session_data["video_file_path"].values[0] + behavior_file_path = session_data["bpod_file_path"].values[0] + + fiber_photometry_metadata = update_default_fiber_photometry_metadata(session_data=session_data) + + try: + date_obj = datetime.strptime(str(session_id), "%Y%m%d") + date_str = date_obj.strftime("%Y-%m-%d") + except ValueError: + raise ValueError( + f"Invalid date format in session_id '{session_id}'. Expected format is 'YYYYMMDD' (e.g. '20210528')." + ) + + subject_metadata = get_subject_metadata_from_rat_info_folder( + folder_path=rat_registry_folder_path, + subject_id=subject_id, + date=date_str, + ) + + yield dict( + raw_fiber_photometry_file_path=raw_fiber_photometry_file_path, + nwbfile_path=nwbfile_path, + raw_behavior_file_path=behavior_file_path, + dlc_file_path=dlc_file_path if not pd.isna(dlc_file_path) else None, + video_file_path=video_file_path if not pd.isna(video_file_path) else None, + fiber_photometry_metadata=fiber_photometry_metadata, + subject_metadata=subject_metadata, + ) + + +if __name__ == "__main__": + + # Parameters for conversion + dataset_excel_file_path = Path("all_sessions_table.xlsx") + output_folder_path = Path("/Users/weian/data/nwbfiles/test") + rat_registry_folder_path = "/Volumes/T9/Constantinople/Rat_info" + + max_workers = 1 + overwrite = True + verbose = False + + dataset_to_nwb( + dataset_excel_file_path=dataset_excel_file_path, + output_folder_path=output_folder_path, + rat_info_folder_path=rat_registry_folder_path, + max_workers=max_workers, + verbose=verbose, + overwrite=overwrite, + ) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_convert_session.py b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_convert_session.py new file mode 100644 index 0000000..8d14f3f --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_convert_session.py @@ -0,0 +1,177 @@ +import os +from pathlib import Path +from typing import Union, Optional + +from dateutil import tz +from neuroconv.utils import load_dict_from_file, dict_deep_update + +from constantinople_lab_to_nwb.fiber_photometry import FiberPhotometryNWBConverter +from ndx_pose import PoseEstimation + +from constantinople_lab_to_nwb.utils import get_subject_metadata_from_rat_info_folder + + +def session_to_nwb( + raw_fiber_photometry_file_path: Union[str, Path], + fiber_photometry_metadata: dict, + raw_behavior_file_path: Union[str, Path], + subject_metadata: dict, + nwbfile_path: Union[str, Path], + dlc_file_path: Optional[Union[str, Path]] = None, + video_file_path: Optional[Union[str, Path]] = None, + stub_test: bool = False, + overwrite: bool = False, + verbose: bool = False, +): + """Converts a fiber photometry session to NWB. + + Parameters + ---------- + raw_fiber_photometry_file_path : Union[str, Path] + Path to the raw fiber photometry file (.doric or .csv). + fiber_photometry_metadata : dict + The metadata for the fiber photometry experiment setup. + subject_metadata: dict + The dictionary containing the subject metadata. (e.g. {'date_of_birth': '2022-11-22', 'description': 'Vendor: OVR', 'sex': 'M'} + raw_behavior_file_path : Union[str, Path] + Path to the raw Bpod output (.mat file). + nwbfile_path : Union[str, Path] + Path to the NWB file. + dlc_file_path : Union[str, Path], optional + Path to the DLC file, by default None. + video_file_path : Union[str, Path], optional + Path to the video file, by default None. + overwrite : bool, optional + Whether to overwrite the NWB file if it already exists, by default False. + verbose : bool, optional + Controls verbosity. + """ + source_data = dict() + conversion_options = dict() + + raw_fiber_photometry_file_path = Path(raw_fiber_photometry_file_path) + + subject_id, session_id = Path(raw_behavior_file_path).stem.split("_", maxsplit=1) + protocol = session_id.split("_")[0] + session_id = session_id.replace("_", "-") + + # Add fiber photometry data + file_suffix = raw_fiber_photometry_file_path.suffix + if file_suffix == ".doric": + interface_name = "FiberPhotometryDoric" + elif file_suffix == ".csv": + interface_name = "FiberPhotometryCsv" + else: + raise ValueError( + f"File '{raw_fiber_photometry_file_path}' extension should be either .doric or .csv and not '{file_suffix}'." + ) + + source_data.update({interface_name: dict(file_path=raw_fiber_photometry_file_path, verbose=verbose)}) + conversion_options.update({interface_name: dict(stub_test=stub_test)}) + + if dlc_file_path is not None: + source_data.update(dict(DeepLabCut=dict(file_path=dlc_file_path))) + + if video_file_path is not None: + source_data.update(dict(Video=dict(file_paths=[video_file_path]))) + + # Add behavior data + source_data.update(dict(Behavior=dict(file_path=raw_behavior_file_path))) + # Exclude some task arguments from the trials table that are the same for all trials + task_arguments_to_exclude = [ + "BlockLengthTest", + "BlockLengthAd", + "TrialsStage2", + "TrialsStage3", + "TrialsStage4", + "TrialsStage5", + "TrialsStage6", + "TrialsStage8", + "CTrial", + ] + conversion_options.update(dict(Behavior=dict(task_arguments_to_exclude=task_arguments_to_exclude))) + + converter = FiberPhotometryNWBConverter(source_data=source_data, verbose=verbose) + + # Add datetime to conversion + metadata = converter.get_metadata() + metadata["NWBFile"].update(session_id=session_id, protocol=protocol) + + session_start_time = metadata["NWBFile"]["session_start_time"] + tzinfo = tz.gettz("America/New_York") + metadata["NWBFile"].update(session_start_time=session_start_time.replace(tzinfo=tzinfo)) + + # Update default metadata with the editable in the corresponding yaml file + editable_metadata_path = Path(__file__).parent / "metadata" / "general_metadata.yaml" + editable_metadata = load_dict_from_file(editable_metadata_path) + metadata = dict_deep_update(metadata, editable_metadata) + + metadata = dict_deep_update(metadata, fiber_photometry_metadata) + + # Update behavior metadata + behavior_metadata_path = Path(__file__).parent / "metadata" / "behavior_metadata.yaml" + behavior_metadata = load_dict_from_file(behavior_metadata_path) + metadata = dict_deep_update(metadata, behavior_metadata) + + metadata["Subject"].update(subject_id=subject_id, **subject_metadata) + + # Run conversion + converter.run_conversion( + nwbfile_path=nwbfile_path, + metadata=metadata, + conversion_options=conversion_options, + overwrite=overwrite, + ) + + +if __name__ == "__main__": + # Parameters for conversion + # Fiber photometry file path + doric_fiber_photometry_file_path = Path( + "/Volumes/T9/Constantinople/Preprocessed_data/J069/Raw/J069_ACh_20230809_HJJ_0002.doric" + ) + # Update default metadata with the editable in the corresponding yaml file + fiber_photometry_metadata_file_path = Path(__file__).parent / "metadata" / "doric_fiber_photometry_metadata.yaml" + fiber_photometry_metadata = load_dict_from_file(fiber_photometry_metadata_file_path) + + # The raw behavior data from Bpod (contains data for a single session) + bpod_behavior_file_path = Path( + "/Volumes/T9/Constantinople/raw_Bpod/J069/DataFiles/J069_RWTautowait2_20230809_131216.mat" + ) + + # DLC file path (optional) + dlc_file_path = Path( + "/Volumes/T9/Constantinople/DeepLabCut/J069/J069-2023-08-09_rig104cam01_0002compDLC_resnet50_GRAB_DA_DMS_RIG104DoricCamera_J029May12shuffle1_500000.h5" + ) + # Behavior video file path (optional) + behavior_video_file_path = Path( + "/Volumes/T9/Constantinople/Compressed Videos/J069/J069-2023-08-09_rig104cam01_0002comp.mp4" + ) + # NWB file path + nwbfile_path = Path("/Users/weian/data/demo/J069_ACh_20230809_HJJ_0002.nwb") + if not nwbfile_path.parent.exists(): + os.makedirs(nwbfile_path.parent, exist_ok=True) + + stub_test = False + overwrite = True + + # Get subject metadata from rat registry + rat_registry_folder_path = "/Volumes/T9/Constantinople/Rat_info" + subject_metadata = get_subject_metadata_from_rat_info_folder( + folder_path=rat_registry_folder_path, + subject_id="J069", + date="2023-08-09", + ) + + session_to_nwb( + raw_fiber_photometry_file_path=doric_fiber_photometry_file_path, + fiber_photometry_metadata=fiber_photometry_metadata, + raw_behavior_file_path=bpod_behavior_file_path, + subject_metadata=subject_metadata, + nwbfile_path=nwbfile_path, + dlc_file_path=dlc_file_path, + video_file_path=behavior_video_file_path, + stub_test=stub_test, + overwrite=overwrite, + verbose=True, + ) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_notes.md b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_notes.md new file mode 100644 index 0000000..e852e86 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_notes.md @@ -0,0 +1,146 @@ +# Notes concerning the fiber_photometry conversion + +## Fiber photometry project + +In this project, the same behavior task is performed (as in mah_2024 conversion, see [notes](https://github.com/catalystneuro/constantinople-lab-to-nwb/blob/58892fa996e0310c9a3047731484bed3ba0a111d/src/constantinople_lab_to_nwb/mah_2024/mah_2024_notes.md)) while + video and fiber photometry is recorded. Video data were acquired using cameras attached to the ceiling of behavior +rigs to capture the top-down view of the arena (Doric USB3 behavior camera, Sony IMX290, recorded with Doric Neuroscience +Studio v6 software). + +To synchronize with neural recordings and Bpod task events, the camera was connected to a digital I/O channel of a +fiber photometry console (FPC) and triggered at 30 Hz via a TTL pulse train. The remaining 3 digital I/O channels of +the FPC were connected to the 3 behavior ports to obtain TTL pulses from port LEDs turning on. The analog channels were +used to record fluorescence. Fluorescence from activity-dependent (GRABDA and GRABACh) and activity-independent +(isosbestic or mCherry) signals was acquired simultaneously via demodulation and downsampled on-the-fly by a factor of 25 +to ~481.9 Hz. The recorded demodulated fluorescence was corrected for photobleaching and motion using Two-channel motion +artifact correction with mCherry or isosbestic signal as the activity-independent channel. + +### Raw fiber photometry data + +The raw fiber photometry data is stored in either .csv files (older version) or .doric files. + +The .csv files contain the following columns: + +`Time(s)` - the time in seconds +`AIn-1 - Dem (AOut-1)` - the demodulated signal from the activity-independent channel +`AIn-1 - Raw` - the raw signal from the activity-independent channel +`AIn-2 - Dem (AOut-2)` - the demodulated signal from the activity-dependent channel +`AIn-2 - Raw` - the raw signal from the activity-dependent channel +`AIn-3` - the isosbestic signal + +The .doric files contain the following fields: + +![doric-example](https://private-user-images.githubusercontent.com/24475788/370304059-9858d9e1-f7d5-484c-b587-1acd093db504.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzIwOTcwMDQsIm5iZiI6MTczMjA5NjcwNCwicGF0aCI6Ii8yNDQ3NTc4OC8zNzAzMDQwNTktOTg1OGQ5ZTEtZjdkNS00ODRjLWI1ODctMWFjZDA5M2RiNTA0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDExMjAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMTIwVDA5NTgyNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTYxOWJmMDk4ZTE4NDdkYTBmMjE0Y2MyNGFmMGE1ODIyYzA2ODRlYTdhMjQzMjU0YTY5M2EzNTUxYzZlMzUxZDEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Aqt7bOsIMSGz7GKK_ttIjxpihAuzzUSCxxG3XqpVWPc) + +For each channel group (e.g. "AnalogIn", "LockInAOUT") the timestamps for the fluosrescence signals can be accessed using the "Time" field. +The "AnalogIn" channel group contains the raw signals and the "LockIn" channel groups contain the demodulated signals. + +`AnalogIn`: + - `AIN01` - raw mCherry signal + - `AIN02` - raw green signal +For dual-fiber experiments the following channels are also present: + - `AIN03` - raw mCherry signal (fiber 2) + - `AIN04` - raw green signal (fiber 2) + +The channels in the "LockIn" channel group can be different from session to session. Here is an example of the channel mapping for a session: + +From `J097_rDAgAChDMSDLS_20240820_HJJ_0000.doric`: +`LockInAOUT01`: + - `Time`: the time in seconds + - `AIN02`: isosbestic signal (fiber 1) + - `AIN04`: isobestic signal (fiber 2) + +`LockInAOUT02`: + - `Time`: the time in seconds + - `AIN02`: motion corrected green signal (fiber 1) + - `AIN04`: motion corrected green signal (fiber 2) + +`LockInAOUT03`: + - `Time`: the time in seconds + - `AIN01`: motion corrected mCherry signal (fiber 1) + +`LockInAOUT04`: + - `Time`: the time in seconds + - `AIN03`: motion corrected mCherry signal (fiber 2) + +From `J069_ACh_20230809_HJJ_0002.doric`: + +TODO: what is the channel mapping here? +`AIN01xAOUT01-LockIn`: + - `Time`: the time in seconds + - `Values`: the isosbestic signal + +`AIN01xAOUT02-LockIn`: + - `Time`: the time in seconds + - `Values`: motion corrected green signal + +`AIN02xAOUT01-LockIn`: + - `Time`: the time in seconds + - `Values`: the isosbestic signal + +`AIN02xAOUT02-LockIn`: + - `Time`: the time in seconds + - `Values`: motion corrected signal + +### Fiber photometry metadata + +The metadata for the fiber photometry data is stored in a `.yaml` file. The metadata contains information about the +fiber photometry setup, such as the LED wavelengths, dichroic mirror, and filter settings. + +The metadata file for the .csv files is named `doric_csv_fiber_photometry_metadata.yaml` and it contains the following fields: + +For each `FiberPhotometryResponseSeries` that we add to NWB, we need to specify the following fields: +- `name` - the name of the FiberPhotometryResponseSeries +- `description` - a description of the FiberPhotometryResponseSeries +- `channel_column_names` - the names of the columns in the .csv file that correspond to the fluorescence signals +- `fiber_photometry_table_region` - the region of the FiberPhotometryTable corresponding to the fluorescence signals +- `fiber_photometry_table_region_description` - a description of the region of the FiberPhotometryTable corresponding to the fluorescence signals + +Example: +```yaml + FiberPhotometryResponseSeries: + - name: fiber_photometry_response_series + description: The raw fluorescence signal + channel_column_names: ["AIn-1 - Raw", "AIn-2 - Raw"] + fiber_photometry_table_region: [0, 1] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw fluorescence signal. + - name: fiber_photometry_response_series_isosbestic + description: The isosbestic signal + channel_column_names: ["AIn-3"] + fiber_photometry_table_region: [0] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the isosbestic signal. + - name: fiber_photometry_response_series_motion_corrected + description: The motion corrected signal + channel_column_names: ["AIn-1 - Dem (AOut-1)", "AIn-2 - Dem (AOut-2)"] + fiber_photometry_table_region: [0, 1] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. +``` + +The metadata file for the .doric files is named `doric_fiber_photometry_metadata.yaml` and it contains the following fields: + +For each `FiberPhotometryResponseSeries` that we add to NWB, we need to specify the following fields: +- `name` - the name of the FiberPhotometryResponseSeries +- `description` - a description of the FiberPhotometryResponseSeries +- `stream_names` - the names of the streams in the .doric file that correspond to the fluorescence signals +- `fiber_photometry_table_region` - the region of the FiberPhotometryTable corresponding to the fluorescence signals +- `fiber_photometry_table_region_description` - a description of the region of the FiberPhotometryTable corresponding to the fluorescence signals + +Example: +```yaml + FiberPhotometryResponseSeries: + - name: fiber_photometry_response_series + description: The raw fluorescence signal # TBD + stream_names: ["AnalogIn/AIN01", "AnalogIn/AIN02", "AnalogIn/AIN03", "AnalogIn/AIN04"] + fiber_photometry_table_region: [0, 1, 2, 3] #[0, 1] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw fluorescence signal. + - name: fiber_photometry_response_series_isosbestic + description: The isosbestic signal # TBD + stream_names: ["LockInAOUT01/AIN02", "LockInAOUT01/AIN04"] + fiber_photometry_table_region: [0, 2] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the isosbestic signal. + - name: fiber_photometry_response_series_motion_corrected + description: The motion corrected signal # TBD + stream_names: ["LockInAOUT03/AIN01", "LockInAOUT04/AIN03", "LockInAOUT02/AIN02", "LockInAOUT02/AIN04"] + fiber_photometry_table_region: [0, 1, 2, 3] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. +``` diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py new file mode 100644 index 0000000..17370f6 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from neuroconv import NWBConverter +from neuroconv.datainterfaces import DeepLabCutInterface, VideoInterface + +from constantinople_lab_to_nwb.fiber_photometry.interfaces import ( + DoricFiberPhotometryInterface, + DoricCsvFiberPhotometryInterface, +) +from constantinople_lab_to_nwb.general_interfaces import BpodBehaviorInterface + + +class FiberPhotometryNWBConverter(NWBConverter): + """Primary conversion class for converting the Fiber photometry dataset from the Constantinople Lab.""" + + data_interface_classes = dict( + DeepLabCut=DeepLabCutInterface, + FiberPhotometryDoric=DoricFiberPhotometryInterface, + FiberPhotometryCsv=DoricCsvFiberPhotometryInterface, + Video=VideoInterface, + Behavior=BpodBehaviorInterface, + ) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.txt b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.txt new file mode 100644 index 0000000..db50c8c --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.txt @@ -0,0 +1,2 @@ +ndx-fiber-photometry==0.1.0 +neuroconv[deeplabcut,video] diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py new file mode 100644 index 0000000..75e528e --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py @@ -0,0 +1 @@ +from .doric_fiber_photometry_interface import DoricFiberPhotometryInterface, DoricCsvFiberPhotometryInterface diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/doric_fiber_photometry_interface.py b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/doric_fiber_photometry_interface.py new file mode 100644 index 0000000..dee3c5b --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/doric_fiber_photometry_interface.py @@ -0,0 +1,176 @@ +from pathlib import Path +from typing import Union + +import h5py +import numpy as np +import pandas as pd +from neuroconv import BaseTemporalAlignmentInterface +from pynwb import NWBFile + +from constantinople_lab_to_nwb.fiber_photometry.utils import ( + add_fiber_photometry_response_series, + add_fiber_photometry_table, +) + + +class DoricFiberPhotometryInterface(BaseTemporalAlignmentInterface): + """Behavior interface for fiber photometry conversion""" + + def __init__( + self, + file_path: Union[str, Path], + root_data_path: str = "/DataAcquisition/FPConsole/Signals/Series0001", + time_column_name: str = "Time", + verbose: bool = True, + ): + super().__init__(file_path=file_path, verbose=verbose) + self._timestamps = None + self._root_data_path = root_data_path + data = self.load() + if root_data_path not in data: + raise ValueError(f"The path '{root_data_path}' not found in '{self.source_data['file_path']}'.") + self._data = data[root_data_path] + self._time_column_name = time_column_name + + def load(self): + file_path = Path(self.source_data["file_path"]) + # check if suffix is .doric + if file_path.suffix != ".doric": + raise ValueError(f"File '{file_path}' is not a .doric file.") + + return h5py.File(file_path, mode="r") + + def get_original_timestamps(self, stream_name: str) -> np.ndarray: + channel_group = self._data[stream_name].parent + if self._time_column_name not in channel_group: + raise ValueError(f"Time column '{self._time_column_name}' not found in '{stream_name}'.") + return channel_group[self._time_column_name][:] + + def get_timestamps(self, stream_name=str, stub_test: bool = False) -> np.ndarray: + timestamps = ( + self._timestamps if self._timestamps is not None else self.get_original_timestamps(stream_name=stream_name) + ) + if stub_test: + return timestamps[:100] + return timestamps + + def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: + self._timestamps = np.array(aligned_timestamps) + + def _get_traces(self, stream_names: list, stub_test: bool = False): + traces_to_add = [] + for stream_name in stream_names: + if stream_name not in self._data: + raise ValueError(f"The path '{stream_name}' not found in '{self.source_data['file_path']}'.") + + trace = self._data[stream_name] + trace = trace[:100] if stub_test else trace[:] + traces_to_add.append(trace) + + traces = np.vstack(traces_to_add).T + return traces + + def add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: dict, + stub_test: bool = False, + ) -> None: + + fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] + add_fiber_photometry_table(nwbfile=nwbfile, metadata=metadata) + + for trace_metadata in fiber_photometry_metadata["FiberPhotometryResponseSeries"]: + fiber_photometry_series_name = trace_metadata["name"] + stream_names = trace_metadata["stream_names"] + + traces = self._get_traces(stream_names=stream_names, stub_test=stub_test) + # Get the timing information + timestamps = self.get_timestamps(stream_name=stream_names[0], stub_test=stub_test) + + add_fiber_photometry_response_series( + traces=traces, + timestamps=timestamps, + nwbfile=nwbfile, + metadata=metadata, + fiber_photometry_series_name=fiber_photometry_series_name, + parent_container="acquisition", + ) + + +class DoricCsvFiberPhotometryInterface(BaseTemporalAlignmentInterface): + + def __init__( + self, + file_path: Union[str, Path], + time_column_name: str = "Time(s)", + verbose: bool = True, + ): + super().__init__(file_path=file_path, verbose=verbose) + self._time_column_name = time_column_name + self._timestamps = None + + def get_original_timestamps(self) -> np.ndarray: + df = self.load() + return df[self._time_column_name].values + + def get_timestamps(self, stub_test: bool = False) -> np.ndarray: + timestamps = self._timestamps if self._timestamps is not None else self.get_original_timestamps() + if stub_test: + return timestamps[:100] + return timestamps + + def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: + self._timestamps = np.array(aligned_timestamps) + + def load(self): + file_path = Path(self.source_data["file_path"]) + # check if suffix is .doric + if file_path.suffix != ".csv": + raise ValueError(f"File '{file_path}' is not a .csv file.") + + df = pd.read_csv( + file_path, + header=1, + index_col=False, + ) + if self._time_column_name not in df.columns: + raise ValueError(f"Time column not found in '{file_path}'.") + return df + + def _get_traces(self, channel_column_names: list, stub_test: bool = False): + traces_to_add = [] + data = self.load() + for channel_name in channel_column_names: + if channel_name not in data.columns: + raise ValueError(f"Channel '{channel_name}' not found in '{self.source_data['file_path']}'.") + trace = data[channel_name] + trace = trace[:100] if stub_test else trace + traces_to_add.append(trace) + + traces = np.vstack(traces_to_add).T + return traces + + def add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: dict, + stub_test: bool = False, + ) -> None: + + fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] + + add_fiber_photometry_table(nwbfile=nwbfile, metadata=metadata) + + for trace_metadata in fiber_photometry_metadata["FiberPhotometryResponseSeries"]: + fiber_photometry_series_name = trace_metadata["name"] + channel_column_names = trace_metadata["channel_column_names"] + + add_fiber_photometry_response_series( + traces=self._get_traces(channel_column_names=channel_column_names, stub_test=stub_test), + timestamps=self.get_timestamps(stub_test=stub_test), + nwbfile=nwbfile, + metadata=metadata, + fiber_photometry_series_name=fiber_photometry_series_name, + parent_container="acquisition", + ) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/behavior_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/behavior_metadata.yaml new file mode 100644 index 0000000..e5cc084 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/behavior_metadata.yaml @@ -0,0 +1,255 @@ +Behavior: + TrialsTable: + description: | + LED illumination from the center port indicated that the animal could initiate a trial by poking its nose in that + port - upon trial initiation the center LED turned off. While in the center port, rats needed to maintain center + fixation for a duration drawn uniformly from [0.8, 1.2] seconds. During the fixation period, a tone played from + both speakers, the frequency of which indicated the volume of the offered water reward for that trial + [1, 2, 4, 8, 16kHz, indicating 5, 10, 20, 40, 80μL rewards]. Following the fixation period, one of the two side + LEDs was illuminated, indicating that the reward might be delivered at that port; the side was randomly chosen on + each trial.This event (side LED ON) also initiated a variable and unpredictable delay period, which was randomly + drawn from an exponential distribution with mean=2.5s. The reward port LED remained illuminated for the duration + of the delay period, and rats were not required to maintain fixation during this period, although they tended to + fixate in the reward port. When reward was available, the reward port LED turned off, and rats could collect the + offered reward by nose poking in that port. The rat could also choose to terminate the trial (opt-out) at any time + by nose poking in the opposite, un-illuminated side port, after which a new trial would immediately begin. On a + proportion of trials (15–25%), the delay period would only end if the rat opted out (catch trials). If rats did + not opt-out within 100s on catch trials, the trial would terminate. The trials were self-paced: after receiving + their reward or opting out, rats were free to initiate another trial immediately. However, if rats terminated + center fixation prematurely, they were penalized with a white noise sound and a time out penalty (typically 2s, + although adjusted to individual animals). Following premature fixation breaks, the rats received the same offered + reward, in order to disincentivize premature terminations for small volume offers. We introduced semi-observable, + hidden states in the task by including uncued blocks of trials with varying reward statistics: high and low blocks + , which offered the highest three or lowest three rewards, respectively, and were interspersed with mixed blocks, + which offered all volumes. There was a hierarchical structure to the blocks, such that high and low blocks + alternated after mixed blocks (e.g., mixed-high-mixed-low, or mixed-low-mixed-high). The first block of each + session was a mixed block. Blocks transitioned after 40 successfully completed trials. Because rats prematurely + broke fixation on a subset of trials, in practice, block durations were variable. + StateTypesTable: + description: Contains the name of the states in the task. + WaitForPoke: + name: wait_for_poke + NoseInCenter: + name: nose_in_center + PunishViolation: + name: punish_violation + GoCue: + name: go_cue + WaitForSidePoke: + name: wait_for_side_poke + PortOut: + name: port_out + AnnounceReward: + name: announce_reward + Reward: + name: reward + OptOut: + name: opt_out + StopSound: + name: stop_sound + StatesTable: + description: Contains the start and end times of each state in the task. + EventTypesTable: + description: Contains the name of the events in the task. + Tup: + name: state_timer + GlobalTimer1_Start: + name: state_timer + GlobalTimer1_End: + name: state_timer + Port1In: + name: left_port_poke + Port1Out: + name: left_port_poke + Port2In: + name: center_port_poke + Port2Out: + name: center_port_poke + Port3In: + name : right_port_poke + Port3Out: + name : right_port_poke + EventsTable: + description: Contains the onset times of events in the task. + ActionTypesTable: + description: Contains the name of the task output actions. + SoundOutput: + name: sound_output + ActionsTable: + description: Contains the onset times of the task output actions (e.g. LED turned on/off). + TaskArgumentsTable: + RewardAmount: + name: reward_volume_ul + description: The volume of reward in microliters. + expression_type: integer + output_type: numeric + NoseInCenter: + name: nose_in_center + description: The time in seconds when the animal is required to maintain center port to initiate the trial (uniformly drawn from 0.8 - 1.2 seconds). + expression_type: double + output_type: numeric + NICincrement: + name: time_increment_for_nose_in_center + description: The time increment for nose in center in seconds. + expression_type: double + output_type: numeric + TargetNIC: + name: target_duration_for_nose_in_center + description: The goal for how long the animal must poke center in seconds. + expression_type: double + output_type: numeric + TrainingStage: + name: training_stage + description: The stage of the training. + expression_type: integer + output_type: numeric + DelayToReward: + name: reward_delay + description: The delay in seconds to receive reward, drawn from exponential distribution with mean = 2.5 seconds. + expression_type: double + output_type: numeric + TargetDelayToReward: + name: target_reward_delay + description: The target delay in seconds to receive reward. + expression_type: double + output_type: numeric + DTRincrement: + name: time_increment_for_reward_delay + description: The time increment during monotonic increase of reward delay. + expression_type: double + output_type: numeric + ViolationTO: + name: violation_time_out + description: The time-out if nose is center is not satisfied in seconds. + expression_type: double + output_type: numeric + Block: + name: block_type + description: The block type (High, Low or Mixed). High and Low blocks are high reward (20, 40, or 80μL) or low reward (5, 10, or 20μL) blocks. The mixed blocks offered all volumes. + expression_type: string + output_type: string + BlockLengthTest: + name: num_trials_in_mixed_blocks + description: The number of trials in each mixed blocks. + expression_type: integer + output_type: numeric + BlockLengthAd: + name: num_trials_in_adaptation_blocks + description: The number of trials in each high reward (20, 40, or 80μL) or low reward (5, 10, or 20μL) blocks. + expression_type: integer + output_type: numeric + PunishSound: + name: punish_sound_enabled + description: Whether to play a white noise pulse on error. + expression_type: boolean + output_type: boolean + ProbCatch: + name: catch_percentage + description: The percentage of catch trials. + expression_type: double + output_type: numeric + IsCatch: + name: is_catch + description: Whether the trial is a catch trial. + expression_type: boolean + output_type: boolean + CTrial: + name: current_trial + description: The current trial number. + expression_type: integer + output_type: numeric + VolumeDelivered: + name: cumulative_reward_volume_ul + description: The cumulative volume received during session in microliters. + expression_type: double + output_type: numeric + WarmUp: + name: is_warm_up + description: Whether the trial is warm-up. + expression_type: boolean + output_type: boolean + OverrideNIC: + name: override_nose_in_center + description: Whether the required time for maintaining center port is overridden. + expression_type: boolean + output_type: boolean + TrialsInStage: + name: trials_in_stage + description: The cumulative number of trials in the stages. + expression_type: integer + output_type: numeric + MinimumVol: + name: min_reward_volume_ul + description: The minimum volume of reward in microliters. (The minimum volume is 4 ul for females and 6 ul for males.) + expression_type: double + output_type: numeric + AutoProbCatch: + name: auto_change_catch_probability + description: Whether to change the probability automatically after a certain number of trials. + expression_type: boolean + output_type: boolean + PrevWasViol: + name: previous_was_violation + description: Whether the previous trial was a violation. + expression_type: boolean + output_type: boolean + changed: + name: changed + description: Whether a block transition occurred for the trial. + expression_type: boolean + output_type: boolean + CPCue: + name: center_port_cue + description: Task parameter. + expression_type: boolean + output_type: boolean + CycleBlocks: + name: cycle_blocks + description: Task parameter. + expression_type: boolean + output_type: boolean + HitFrac: + name: hit_percentage + description: The percentage of hit trials. + expression_type: double + output_type: numeric + hits: + name: hits + description: The number of trials where reward was delivered. + expression_type: integer + output_type: numeric + TrialsStage2: + name: num_trials_in_stage_2 + description: Determines how many trials occur in stage 2 before transition. + expression_type: integer + output_type: numeric + TrialsStage3: + name: num_trials_in_stage_3 + description: Determines how many trials occur in stage 3 before transition. + expression_type: integer + output_type: numeric + TrialsStage4: + name: num_trials_in_stage_4 + description: Determines how many trials occur in stage 4 before transition. + expression_type: integer + output_type: numeric + TrialsStage5: + name: num_trials_in_stage_5 + description: Determines how many trials occur in stage 5 before transition. + expression_type: integer + output_type: numeric + TrialsStage6: + name: num_trials_in_stage_6 + description: Determines how many trials occur in stage 6 before transition. + expression_type: integer + output_type: numeric + TrialsStage8: + name: num_trials_in_stage_8 + description: Determines how many trials occur in stage 8 before transition. + expression_type: integer + output_type: numeric + HiITI: + name: high_ITI + description: Task parameter. # no description provided + expression_type: double + output_type: numeric diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_csv_fiber_photometry_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_csv_fiber_photometry_metadata.yaml new file mode 100644 index 0000000..7457e98 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_csv_fiber_photometry_metadata.yaml @@ -0,0 +1,97 @@ +# The metadata of the fiber photometry setup +# papers with similar setup: https://www.biorxiv.org/content/10.1101/2023.12.09.570945v1.full.pdf +# https://www.biorxiv.org/content/10.1101/2024.05.03.592444v1.full.pdf +Ophys: + FiberPhotometry: + OpticalFibers: + - name: optical_fiber + description: Chronically implantable optic fibers (Thor labs) with 400 µm core, 0.5 NA fiber optics were implanted unilaterally over the injection site (DV -6.7). Doric Lenses hardware and software (Doric Neuroscience Studio) were used to record fluorescence. Two-channel motion artifact correction (TMAC) was used to correct for movement artifacts, with mCherry as the activity-independent channel. + manufacturer: Thor labs + # model: unknown + numerical_aperture: 0.5 + core_diameter_in_um: 400.0 + ExcitationSources: # Can't find any information in the referred papers + - name: excitation_source_grab_da + description: The excitation wavelength for GRAB-DA indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 470.0 + - name: excitation_source_mcherry + description: The excitation wavelength for mCherry indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 405.0 + - name: excitation_source_grab_da_isosbestic + description: The isosbestic point for GRAB-DA indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 405.0 + Photodetectors: # Can't find any information in the referred papers + - name: photodetector + # description: TBD + manufacturer: Doric Lenses + # model: TBD + detector_type: photodiode + detected_wavelength_in_nm: 470.0 # TBD + # gain: # TBD + DichroicMirrors: # Can't find any information in the referred papers + - name: dichroic_mirror + # description: TBD + manufacturer: Doric Lenses + # model: TBD + Indicators: + - name: grab_da + description: "To measure dopamine activity, AAV9-hsyn-GRAB-DA2h (AddGene #140554) was injected into the NAcc." + manufacturer: Addgene + label: AAV9-hsyn-GRAB-DA2h + injection_location: NAcc + # injection_coordinates_in_mm: [-0.4, -3.5, -4.6] + - name: mcherry + description: "The control fluorophore was mCherry (AAV1-CB7-CI-mCherry-WPRE-RBG, AddGene #105544)." + label: AAV1-CB7-CI-mCherry + manufacturer: Addgene + # injection_coordinates_in_mm: [-0.4, -3.5, -4.6] + FiberPhotometryTable: + name: fiber_photometry_table + description: The metadata of the fiber photometry setup. + rows: + - name: 0 + location: NAcc + # coordinates: [0.8, 1.5, 2.8] + indicator: mcherry + optical_fiber: optical_fiber + excitation_source: excitation_source_mcherry + photodetector: photodetector + dichroic_mirror: dichroic_mirror + - name: 1 + location: NAcc + # coordinates: [0.8, 1.5, 2.8] + indicator: grab_da + optical_fiber: optical_fiber + excitation_source: excitation_source_grab + photodetector: photodetector + dichroic_mirror: dichroic_mirror + - name: 2 + location: NAcc + # coordinates: [0.8, 1.5, 2.8] + indicator: grab_da + optical_fiber: optical_fiber + excitation_source: excitation_source_grab_isosbestic + photodetector: photodetector + dichroic_mirror: dichroic_mirror + FiberPhotometryResponseSeries: + - name: raw_fiber_photometry_signal + description: The raw fiber photometry signal before demodulation. # TBD + channel_column_names: ["AIn-1 - Raw", "AIn-2 - Raw", "AIn-3"] + unit: a.u. + fiber_photometry_table_region: [0, 1, 2] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw signal. + - name: estimated_fiber_photometry_response_series + description: The demodulated (estimated) signal from light stimulation using a proprietary algorithm from Doric. + channel_column_names: ["AIn-1 - Dem (AOut-1)", "AIn-2 - Dem (AOut-2)"] + unit: a.u. + fiber_photometry_table_region: [0, 1] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the estimated signal. diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_fiber_photometry_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_fiber_photometry_metadata.yaml new file mode 100644 index 0000000..a69357f --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_fiber_photometry_metadata.yaml @@ -0,0 +1,114 @@ +# The metadata of the fiber photometry setup +# papers with similar setup: https://www.biorxiv.org/content/10.1101/2023.12.09.570945v1.full.pdf +# https://www.biorxiv.org/content/10.1101/2024.05.03.592444v1.full.pdf +Ophys: + FiberPhotometry: + OpticalFibers: + - name: optical_fiber + description: Chronically implantable optic fibers (Thor labs) with 400 µm core, 0.5 NA fiber optics were implanted unilaterally over the injection site (DV -6.7). Doric Lenses hardware and software (Doric Neuroscience Studio) were used to record fluorescence. Two-channel motion artifact correction (TMAC) was used to correct for movement artifacts, with mCherry as the activity-independent channel. + manufacturer: Thor labs + # model: unknown + numerical_aperture: 0.5 + core_diameter_in_um: 400.0 + ExcitationSources: # Can't find any information in the referred papers + - name: excitation_source_grab_da + description: The excitation wavelength for GRAB-DA indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 470.0 + - name: excitation_source_grab_ach + description: The excitation wavelength for GRAB-ACh indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 470.0 + - name: excitation_source_mcherry + description: The excitation wavelength for mCherry indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 405.0 + - name: excitation_source_grab_da_isosbestic + description: The isosbestic point for GRAB-DA indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 405.0 + - name: excitation_source_grab_ach_isosbestic + description: The isosbestic point for GRAB-ACh indicator. + manufacturer: Doric Lenses + # model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 405.0 + Photodetectors: # Can't find any information in the referred papers + - name: photodetector + # description: TBD + manufacturer: Doric Lenses + # model: TBD + detector_type: photodiode + detected_wavelength_in_nm: 470.0 # TBD + # gain: # TBD + DichroicMirrors: # Can't find any information in the referred papers + - name: dichroic_mirror + # description: TBD + manufacturer: Doric Lenses + # model: TBD + Indicators: + - name: grab_da + description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the DMS." + manufacturer: Addgene + label: AAV9-hsyn-GRAB-DA2h + injection_location: DMS + injection_coordinates_in_mm: [-0.4, -3.5, -4.6] + - name: grab_ach + description: "To measure acetylcholine activity, AAV9-hSyn-ACh4.3 (WZ Biosciences #YL10002) was injected into the DMS." + manufacturer: WZ Biosciences + label: AAV9-hSyn-ACh4.3 + injection_location: DMS + injection_coordinates_in_mm: [1.7, -2.8, -4.3] + - name: mcherry + description: "The control fluorophore AAV9-CB7-CI-mCherry-WPRE-RBG (AddGene #105544)" + label: AAV1-CB7-CI-mCherry + manufacturer: Addgene + # injection_coordinates_in_mm: [-0.4, -3.5, -4.6] + FiberPhotometryTable: + name: fiber_photometry_table + description: The metadata of the fiber photometry setup. + rows: + - name: 0 + location: DMS + # coordinates: [0.8, 1.5, 2.8] + indicator: mcherry + optical_fiber: optical_fiber + excitation_source: excitation_source_mcherry + photodetector: photodetector + dichroic_mirror: dichroic_mirror + - name: 1 + location: DMS + # coordinates: [0.8, 1.5, 2.8] + indicator: grab_ach + optical_fiber: optical_fiber + excitation_source: excitation_source_grab_ach + photodetector: photodetector + dichroic_mirror: dichroic_mirror + - name: 2 + location: DMS + indicator: grab_ach + optical_fiber: optical_fiber + excitation_source: excitation_source_grab_ach_isosbestic + photodetector: photodetector + dichroic_mirror: dichroic_mirror + FiberPhotometryResponseSeries: + - name: raw_fiber_photometry_signal + description: The raw fiber photometry signal from Doric acquisition system before demodulation. # TBD + stream_names: ["AnalogIn/AIN01", "AnalogIn/AIN02"] + unit: a.u. + fiber_photometry_table_region: [0, 1] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw signal. + - name: estimated_fiber_photometry_response_series + description: The demodulated (estimated) signal from light stimulation using a proprietary algorithm from Doric. + stream_names: ["AIN01xAOUT01-LockIn/Values", "AIN01xAOUT02-LockIn/Values", "AIN02xAOUT01-LockIn/Values", "AIN02xAOUT02-LockIn/Values"] + unit: a.u. + fiber_photometry_table_region: [0, 1, 0, 2] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the estimated signal. diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/general_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/general_metadata.yaml new file mode 100644 index 0000000..8ab9216 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/general_metadata.yaml @@ -0,0 +1,21 @@ +NWBFile: +# related_publications: +# - https://doi.org/10.1038/s41467-023-43250-x +# - https://doi.org/10.5281/zenodo.10031483 + experiment_description: | + This dataset contains fiber photometry recordings during decision-making behavioral task in rats. Deeplabcut software (v.2.2.3) was used for tracking the behavior ports (right port, central port, and left port) and 6 body parts (right ear, nose, left ear, mid-point along the right torso, mid-point along the left torso, and base of the tail). Video data were acquired using cameras attached to the ceiling of behavior rigs to capture the top-down view of the arena (Doric USB3 behavior camera, Sony IMX290, recorded with Doric Neuroscience Studio v6 software). The fluorescence from activity-dependent (GRAB-DA and GRAB-ACh) and activity-independent (isosbestic or mCherry) signals was acquired simultaneously via demodulation and downsampled on-the-fly by a factor of 25 to ~481.9 Hz. The behavioral tasks were conducted in a high-throughput facility where rats were trained in increasingly complex protocols. Trials were initiated by a nose-poke in a lit center port and required maintaining a center fixation for 0.8 to 1.2 seconds, during which a tone indicated the possible reward size. A subsequent side LED indicated the potential reward location, followed by a delay period drawn from an exponential distribution (mean = 2.5 s). Rats could opt out at any time by poking the unlit port, restarting the trial. Catch trials, where the delay period only ended if the rat opted out, constituted 15-25% of the trials. Rats received penalties for premature fixation breaks. Additionally, the tasks introduced semi-observable hidden states by varying reward statistics across uncued blocks (high, low, and mixed), structured hierarchically, with blocks transitioning after 40 successfully completed trials. + session_description: + This session contains fiber photometry recordings during decision-making behavioral task in rats. Deeplabcut software (v.2.2.3) was used for tracking the behavior ports (right port, central port, and left port) and 6 body parts (right ear, nose, left ear, mid-point along the right torso, mid-point along the left torso, and base of the tail). Video data were acquired using cameras attached to the ceiling of behavior rigs to capture the top-down view of the arena (Doric USB3 behavior camera, Sony IMX290, recorded with Doric Neuroscience Studio v6 software). The fluorescence from activity-dependent (GRAB-DA and GRAB-ACh) and activity-independent (isosbestic or mCherry) signals was acquired simultaneously via demodulation and downsampled on-the-fly by a factor of 25 to ~481.9 Hz. + keywords: + - decision making + - reinforcement learning + - hidden state inference + - fiber photometry + institution: NYU Center for Neural Science + lab: Constantinople + experimenter: + - Mah, Andrew +Subject: + species: Rattus norvegicus + # age: in ISO 8601 format, updated automatically for each subject + # sex: One of M, F, U, or O, updated automatically for each subject diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py new file mode 100644 index 0000000..f724705 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py @@ -0,0 +1,5 @@ +from .add_fiber_photometry import ( + add_fiber_photometry_devices, + add_fiber_photometry_table, + add_fiber_photometry_response_series, +) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/utils/add_fiber_photometry.py b/src/constantinople_lab_to_nwb/fiber_photometry/utils/add_fiber_photometry.py new file mode 100644 index 0000000..c948360 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/utils/add_fiber_photometry.py @@ -0,0 +1,175 @@ +from typing import Literal + +import numpy as np +from ndx_fiber_photometry import FiberPhotometryTable, FiberPhotometry, FiberPhotometryResponseSeries +from neuroconv.tools import get_module +from neuroconv.tools.fiber_photometry import add_fiber_photometry_device +from neuroconv.tools.hdmf import SliceableDataChunkIterator +from pynwb import NWBFile + + +def add_fiber_photometry_devices(nwbfile: NWBFile, metadata: dict): + """ + Add fiber photometry devices to the NWBFile based on the metadata dictionary. + The devices include OpticalFiber, ExcitationSource, Photodetector, BandOpticalFilter, EdgeOpticalFilter, + DichroicMirror, and Indicator. Device metadata is extracted from the "Ophys" section of the main metadata dictionary. + For example, metadata["Ophys"]["FiberPhotometry"]["OpticalFibers"] should contain a list of dictionaries, each + containing metadata for an OpticalFiber device. + + Parameters + ---------- + nwbfile : NWBFile + The NWBFile where the fiber photometry devices will be added. + + metadata : dict + Dictionary containing metadata for the NWB file. Should include an "Ophys" + key, which in turn should contain a "FiberPhotometry" key with detailed + metadata for each device type. + """ + fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] + # Add Devices + device_types = [ + "OpticalFiber", + "ExcitationSource", + "Photodetector", + "BandOpticalFilter", + "EdgeOpticalFilter", + "DichroicMirror", + "Indicator", + ] + for device_type in device_types: + devices_metadata = fiber_photometry_metadata.get(device_type + "s", []) + for device_metadata in devices_metadata: + add_fiber_photometry_device( + nwbfile=nwbfile, + device_metadata=device_metadata, + device_type=device_type, + ) + + +def add_fiber_photometry_table(nwbfile: NWBFile, metadata: dict): + """ + Adds a `FiberPhotometryTable` to the NWB file based on the metadata dictionary. + The metadata for the `FiberPhotometryTable` should be located in metadata["Ophys"]["FiberPhotometry"]["FiberPhotometryTable"]. + + Parameters + ---------- + nwbfile : NWBFile + The NWB file where the `FiberPhotometryTable` will be added. + metadata : dict + A dictionary containing metadata necessary for constructing the Fiber Photometry + table. Expects keys "Ophys" and "FiberPhotometry" with appropriate subkeys. + """ + add_fiber_photometry_devices(nwbfile=nwbfile, metadata=metadata) + + fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] + fiber_photometry_table_metadata = fiber_photometry_metadata["FiberPhotometryTable"] + + if "FiberPhotometry" in nwbfile.lab_meta_data: + return + + fiber_photometry_table = FiberPhotometryTable( + name=fiber_photometry_table_metadata["name"], + description=fiber_photometry_table_metadata["description"], + ) + + fiber_photometry_lab_meta_data = FiberPhotometry( + name="FiberPhotometry", + fiber_photometry_table=fiber_photometry_table, + ) + nwbfile.add_lab_meta_data(fiber_photometry_lab_meta_data) + + fiber_photometry_table = nwbfile.lab_meta_data["FiberPhotometry"].fiber_photometry_table + rows_metadata = fiber_photometry_metadata["FiberPhotometryTable"]["rows"] + device_fields = [ + "optical_fiber", + "excitation_source", + "photodetector", + "dichroic_mirror", + "indicator", + "excitation_filter", + "emission_filter", + ] + for row_metadata in rows_metadata: + row_data = {field: nwbfile.devices[row_metadata[field]] for field in device_fields if field in row_metadata} + row_data.update(location=row_metadata["location"]) + if "coordinates" in row_metadata: + row_data["coordinates"] = row_metadata["coordinates"] + fiber_photometry_table.add_row(**row_data, id=int(row_metadata["name"])) + + +def add_fiber_photometry_response_series( + traces: np.ndarray, + timestamps: np.ndarray, + nwbfile: NWBFile, + metadata: dict, + fiber_photometry_series_name: str, + parent_container: Literal["acquisition", "processing/ophys"] = "acquisition", +): + """ + Adds a `FiberPhotometryResponseSeries` to the NWBFile. This function first adds the necessary devices + and metadata to the NWBFile. Then, it creates a FiberPhotometryTable and adds rows to it based on the provided + metadata. Finally, it adds the FiberPhotometryResponseSeries to the specified container in the NWBFile. + + Parameters + ---------- + traces : np.ndarray + Numpy array containing the fiber photometry data. + timestamps : np.ndarray + Numpy array containing the timestamps corresponding to the fiber photometry data. + nwbfile : NWBFile + The NWBFile object to which the FiberPhotometryResponseSeries will be added. + metadata : dict + Dictionary containing metadata required for adding the FiberPhotometry devices, table, + and FiberPhotometryResponseSeries. + fiber_photometry_series_name : str + Name of the FiberPhotometryResponseSeries to be added. + parent_container : Literal["acquisition", "processing/ophys"], optional + Specifies the container within the NWBFile where the FiberPhotometryResponseSeries will be added. + Default is "acquisition". + + Raises + ------ + ValueError + If trace metadata for the specified series name is not found. + If the number of channels in traces does not match the number of rows in the fiber photometry table. + If the lengths of traces and timestamps do not match. + """ + fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] + traces_metadata = fiber_photometry_metadata["FiberPhotometryResponseSeries"] + trace_metadata = next( + (trace for trace in traces_metadata if trace["name"] == fiber_photometry_series_name), + None, + ) + if trace_metadata is None: + raise ValueError(f"Trace metadata for '{fiber_photometry_series_name}' not found.") + + fiber_photometry_table = nwbfile.lab_meta_data["FiberPhotometry"].fiber_photometry_table + assert fiber_photometry_table is not None, "'FiberPhotometryTable' not found in lab meta data." + + fiber_photometry_table_region = fiber_photometry_table.create_fiber_photometry_table_region( + description=trace_metadata["fiber_photometry_table_region_description"], + region=list(trace_metadata["fiber_photometry_table_region"]), + ) + + if traces.shape[0] != len(timestamps): + raise ValueError(f"Length of traces ({len(traces)}) and timestamps ({len(timestamps)}) should be equal.") + + fiber_photometry_response_series = FiberPhotometryResponseSeries( + name=trace_metadata["name"], + description=trace_metadata["description"], + data=SliceableDataChunkIterator(data=traces), + unit=trace_metadata["unit"], + fiber_photometry_table_region=fiber_photometry_table_region, + timestamps=timestamps, + ) + + if parent_container == "acquisition": + nwbfile.add_acquisition(fiber_photometry_response_series) + elif parent_container == "processing/ophys": + ophys_module = get_module( + nwbfile, + name="ophys", + description="Contains the processed fiber photometry data.", + ) + ophys_module.add(fiber_photometry_response_series)