From 17b41bfc7fa8e809a231578ab1d11b602c0418fe Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 25 Sep 2024 18:24:57 +0200 Subject: [PATCH 01/29] add interface --- .../fiber_photometry/__init__.py | 0 .../fiber_photometry_convert_session.py | 0 .../fiber_photometry_nwbconverter.py | 0 .../fiber_photometry_requirements.py | 0 .../fiber_photometry/interfaces/__init__.py | 0 .../doric_fiber_photometry_interface.py | 152 ++++++++++++++++++ .../metadata/fiber_photometry_metadata.yaml | 144 +++++++++++++++++ .../fiber_photometry/utils/__init__.py | 1 + .../utils/add_fiber_photometry.py | 48 ++++++ 9 files changed, 345 insertions(+) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/__init__.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_convert_session.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/interfaces/doric_fiber_photometry_interface.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/utils/add_fiber_photometry.py 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..e69de29 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..e69de29 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..e69de29 diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.py b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.py new file mode 100644 index 0000000..e69de29 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..e69de29 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..891f58a --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/doric_fiber_photometry_interface.py @@ -0,0 +1,152 @@ +from pathlib import Path +from typing import Union, Literal + +import h5py +import numpy as np +from ndx_fiber_photometry import FiberPhotometryResponseSeries +from neuroconv import BaseTemporalAlignmentInterface +from neuroconv.tools import get_module +from neuroconv.utils import load_dict_from_file, dict_deep_update +from pynwb import NWBFile + +from constantinople_lab_to_nwb.fiber_photometry.utils import add_fiber_photometry_table, add_fiber_photometry_devices + + +class DoricFiberPhotometryInterface(BaseTemporalAlignmentInterface): + """Behavior interface for fiber photometry conversion""" + + def __init__( + self, + file_path: Union[str, Path], + stream_name: str, + verbose: bool = True, + ): + self._timestamps = None + super().__init__(file_path=file_path, stream_name=stream_name, verbose=verbose) + + def load(self, stream_name: str): + 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.") + + channel_group = h5py.File(file_path, mode="r")[stream_name] + if "Time" not in channel_group.keys(): + raise ValueError(f"Time not found in '{stream_name}'.") + return channel_group + + def get_original_timestamps(self) -> np.ndarray: + channel_group = self.load(stream_name=self.source_data["stream_name"]) + return channel_group["Time"][:] + + 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 add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: dict, + fiber_photometry_series_name: str, + parent_container: Literal["acquisition", "processing/ophys"] = "acquisition", + stub_test: bool = False, + ) -> None: + + add_fiber_photometry_devices(nwbfile=nwbfile, metadata=metadata) + + 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.") + + add_fiber_photometry_table(nwbfile=nwbfile, metadata=metadata) + fiber_photometry_table = nwbfile.lab_meta_data["FiberPhotometry"].fiber_photometry_table + + row_indices = trace_metadata["fiber_photometry_table_region"] + device_fields = [ + "optical_fiber", + "excitation_source", + "photodetector", + "dichroic_mirror", + "indicator", + "excitation_filter", + "emission_filter", + ] + for row_index in row_indices: + row_metadata = fiber_photometry_metadata["FiberPhotometryTable"]["rows"][row_index] + row_data = {field: nwbfile.devices[row_metadata[field]] for field in device_fields if field in row_metadata} + row_data["location"] = row_metadata["location"] + if "coordinates" in row_metadata: + row_data["coordinates"] = row_metadata["coordinates"] + if "commanded_voltage_series" in row_metadata: + row_data["commanded_voltage_series"] = nwbfile.acquisition[row_metadata["commanded_voltage_series"]] + fiber_photometry_table.add_row(**row_data) + + stream_name = trace_metadata["stream_name"] + stream_indices = trace_metadata["stream_indices"] + + traces_to_add = [] + data = self.load(stream_name=stream_name) + channel_names = list(data.keys()) + for stream_index in stream_indices: + trace = data[channel_names[stream_index]] + trace = trace[:100] if stub_test else trace[:] + traces_to_add.append(trace) + + traces = np.vstack(traces_to_add).T + + fiber_photometry_table_region = fiber_photometry_table.create_fiber_photometry_table_region( + description=trace_metadata["fiber_photometry_table_region_description"], + region=trace_metadata["fiber_photometry_table_region"], + ) + + # Get the timing information + timestamps = self.get_timestamps(stub_test=stub_test) + + fiber_photometry_response_series = FiberPhotometryResponseSeries( + name=trace_metadata["name"], + description=trace_metadata["description"], + 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) + + +interface = DoricFiberPhotometryInterface( + file_path="/Volumes/T9/Constantinople/Preprocessed_data/J100/Raw/J100_rDAgAChDLSDMS_20240819_HJJ_0001.doric", + stream_name="/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn/", +) + +metadata = interface.get_metadata() +metadata["NWBFile"]["session_start_time"] = "2024-08-19T00:00:00" + +# Update default metadata with the editable in the corresponding yaml file +editable_metadata_path = Path(__file__).parent.parent / "metadata" / "fiber_photometry_metadata.yaml" +editable_metadata = load_dict_from_file(editable_metadata_path) +metadata = dict_deep_update(metadata, editable_metadata) + +interface.run_conversion( + nwbfile_path="/Volumes/T9/Constantinople/nwbfiles/test.nwb", + metadata=metadata, + fiber_photometry_series_name="fiber_photometry_response_series_green", +) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml new file mode 100644 index 0000000..a882764 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml @@ -0,0 +1,144 @@ +# 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: The optical fibers (Thor labs) with 400 μm core, 0.5 NA fiber optics were implanted over the injection site. + 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 + description: TBD + manufacturer: Doric Lenses + model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 0.0 # TBD + - name: excitation_source_isosbestic_control + description: TBD + manufacturer: Doric Lenses + model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 0.0 # TBD + 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: 0.0 # TBD + # gain: # TBD + BandOpticalFilters: # Can't find any information in the referred papers + - name: emission_filter + description: TBD + manufacturer: Doric Lenses + model: TBD + center_wavelength_in_nm: 0.0 # TBD + bandwidth_in_nm: 0.0 # TBD + filter_type: Bandpass + - name: excitation_filter + description: TBD + manufacturer: Doric Lenses + model: TBD + center_wavelength_in_nm: 0.0 # TBD + bandwidth_in_nm: 0.0 # TBD + filter_type: Bandpass + - name: isosbestic_excitation_filter + description: TBD + manufacturer: Doric Lenses + model: TBD + center_wavelength_in_nm: 0.0 # TBD + bandwidth_in_nm: 0.0 # TBD + filter_type: Bandpass + DichroicMirrors: # Can't find any information in the referred papers + - name: dichroic_mirror + description: TBD + manufacturer: Doric Lenses + model: TBD + Indicators: + - name: grab_da_dms + description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the DMS (AP: -0.4 mm, ML: ±3.5 mm, DV: -4.6 mm from the skull surface at bregma at a 10 degrees angle)." + manufacturer: Addgene + label: AAV9-hSyn-GRAB + injection_location: DMS + injection_coordinates_in_mm: [-0.4, -3.5, -4.6] + - name: grab_ach_dms + description: "To measure acetylcholine activity, AAV9-hSyn-ACh4.3 (WZ Biosciences #YL10002) was injected into the DMS (AP: 1.7 mm, ML: ±2.8 mm, DV: -4.3 mm from the skull surface at bregma at a 10 degrees angle)." + manufacturer: WZ Biosciences + label: AAV9-hSyn-ACh4.3 + injection_location: DMS + injection_coordinates_in_mm: [1.7, 2.8, -4.3] + - name: grab_da_nacc + description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the NAcc (AP: 1.3 mm, ML: 1.65 mm, DV:-6.9 mm with an 8-10 degrees angle from the midline for bilateral implants)." + manufacturer: Addgene + label: AAV9-hSyn-GRAB + injection_location: NAcc + injection_coordinates_in_mm: [1.3, 1.65, -6.9] + - name: mcherry + description: "The control fluorophore was mCherry (AAV9-CB7-CI-mCherry-WPRE-RBG, AddGene #105544)." + label: AAV1-CB7-CI-mCherry + manufacturer: Addgene + FiberPhotometryTable: + name: fiber_photometry_table + description: TBD + rows: + - name: 0 + location: DMS + # coordinates: [0.8, 1.5, 2.8] + # commanded_voltage_series: commanded_voltage_series_dms_calcium_signal + indicator: grab_da_dms + optical_fiber: optical_fiber + excitation_source: excitation_source + photodetector: photodetector + excitation_filter: excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + - name: 1 + location: DLS + # coordinates: [0.8, 1.5, 2.8] + indicator: grab_da_dms + optical_fiber: optical_fiber + excitation_source: excitation_source + photodetector: photodetector + excitation_filter: excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + - name: 2 + location: DMS + # coordinates: [0.8, 1.5, 2.8] + # commanded_voltage_series: commanded_voltage_series_dms_isosbestic_control + indicator: mcherry + optical_fiber: optical_fiber + excitation_source: excitation_source_isosbestic_control + photodetector: photodetector + excitation_filter: isosbestic_excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + - name: 3 + location: DLS + # coordinates: [0.8, 1.5, 2.8] + indicator: mcherry + optical_fiber: optical_fiber + excitation_source: excitation_source_isosbestic_control + photodetector: photodetector + excitation_filter: isosbestic_excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + FiberPhotometryResponseSeries: + - name: fiber_photometry_response_series_green + description: The raw fluorescence signal # TBD + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn/" + stream_indices: [1, 3] + unit: a.u. + 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_red + description: The raw fluorescence signal # TBD + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn/" + stream_indices: [0, 2] + unit: a.u. + fiber_photometry_table_region: [2, 3] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw fluorescence signal. 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..4bee0ff --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py @@ -0,0 +1 @@ +from .add_fiber_photometry import add_fiber_photometry_devices, add_fiber_photometry_table 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..8ec8d6a --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/utils/add_fiber_photometry.py @@ -0,0 +1,48 @@ +from ndx_fiber_photometry import FiberPhotometryTable, FiberPhotometry +from neuroconv.tools.fiber_photometry import add_fiber_photometry_device +from pynwb import NWBFile + + +def add_fiber_photometry_devices(nwbfile: NWBFile, metadata: dict): + 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): + 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_table.add_column( + # name="additional_column_name", + # description="additional_column_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) From b848410af3df7e3b80f565ef5654a4ad2ebe9527 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sun, 29 Sep 2024 15:40:39 +0200 Subject: [PATCH 02/29] add requirements --- .../fiber_photometry/fiber_photometry_requirements.py | 0 .../fiber_photometry/fiber_photometry_requirements.txt | 2 ++ 2 files changed, 2 insertions(+) delete mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.py create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.txt diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.py b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_requirements.py deleted file mode 100644 index e69de29..0000000 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] From b60c298a5c85f1ac45fe099d9b2c6074a2507f6e Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sun, 29 Sep 2024 15:48:58 +0200 Subject: [PATCH 03/29] add converter --- .../fiber_photometry_nwbconverter.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 index e69de29..3333024 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py @@ -0,0 +1,14 @@ +from neuroconv import NWBConverter +from neuroconv.datainterfaces import DeepLabCutInterface, VideoInterface + +from constantinople_lab_to_nwb.fiber_photometry.interfaces import DoricFiberPhotometryInterface + + +class FiberPhotometryNWBConverter(NWBConverter): + """Primary conversion class for converting the Fiber photometry dataset from the Constantinople Lab.""" + + data_interface_classes = dict( + FiberPhotometry=DoricFiberPhotometryInterface, + DeepLabCut=DeepLabCutInterface, + Video=VideoInterface, + ) From 93be6e961bd3d0e550c9c8f16b977d2fb8915696 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sun, 29 Sep 2024 15:49:14 +0200 Subject: [PATCH 04/29] add convert session script --- .../fiber_photometry_convert_session.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) 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 index e69de29..3650422 100644 --- 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 @@ -0,0 +1,132 @@ +import os +import re +from datetime import datetime +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 + + +def session_to_nwb( + raw_fiber_photometry_file_path: Union[str, Path], + 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. + 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) + raw_fiber_photometry_file_name = raw_fiber_photometry_file_path.stem + subject_id, session_id = raw_fiber_photometry_file_name.split("_", maxsplit=1) + session_id = session_id.replace("_", "-") + + # Add fiber photometry data + source_data.update( + dict( + FiberPhotometry=dict( + file_path=raw_fiber_photometry_file_path, + stream_name="/DataAcquisition/FPConsole/Signals/Series0001/AIN01xAOUT01-LockIn", + ) + ) + ) + conversion_options.update( + dict( + FiberPhotometry=dict( + stub_test=stub_test, + fiber_photometry_series_name="fiber_photometry_response_series_green", + ) + ) + ) + + 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]))) + + converter = FiberPhotometryNWBConverter(source_data=source_data, verbose=verbose) + + # Add datetime to conversion + metadata = converter.get_metadata() + metadata["NWBFile"].update(session_id=session_id) + + date_pattern = r"(?P\d{8})" + + match = re.search(date_pattern, raw_fiber_photometry_file_name) + if match: + date_str = match.group("date") + date_obj = datetime.strptime(date_str, "%Y%m%d") + session_start_time = date_obj + 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" / "fiber_photometry_metadata.yaml" + editable_metadata = load_dict_from_file(editable_metadata_path) + metadata = dict_deep_update(metadata, editable_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" + ) + # 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("/Volumes/T9/Constantinople/nwbfiles/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 + + session_to_nwb( + raw_fiber_photometry_file_path=doric_fiber_photometry_file_path, + 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, + ) From 39bef1e5d52f753c34b12ba5b1d1b140394b3cbb Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sun, 29 Sep 2024 15:49:30 +0200 Subject: [PATCH 05/29] update --- .../fiber_photometry/__init__.py | 1 + .../fiber_photometry/interfaces/__init__.py | 1 + .../doric_fiber_photometry_interface.py | 21 ------------------- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/__init__.py index e69de29..aa4e25b 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/__init__.py +++ 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/interfaces/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py index e69de29..201cd5d 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py @@ -0,0 +1 @@ +from .doric_fiber_photometry_interface import DoricFiberPhotometryInterface 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 index 891f58a..635ec9e 100644 --- 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 @@ -6,7 +6,6 @@ from ndx_fiber_photometry import FiberPhotometryResponseSeries from neuroconv import BaseTemporalAlignmentInterface from neuroconv.tools import get_module -from neuroconv.utils import load_dict_from_file, dict_deep_update from pynwb import NWBFile from constantinople_lab_to_nwb.fiber_photometry.utils import add_fiber_photometry_table, add_fiber_photometry_devices @@ -130,23 +129,3 @@ def add_to_nwbfile( description="Contains the processed fiber photometry data.", ) ophys_module.add(fiber_photometry_response_series) - - -interface = DoricFiberPhotometryInterface( - file_path="/Volumes/T9/Constantinople/Preprocessed_data/J100/Raw/J100_rDAgAChDLSDMS_20240819_HJJ_0001.doric", - stream_name="/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn/", -) - -metadata = interface.get_metadata() -metadata["NWBFile"]["session_start_time"] = "2024-08-19T00:00:00" - -# Update default metadata with the editable in the corresponding yaml file -editable_metadata_path = Path(__file__).parent.parent / "metadata" / "fiber_photometry_metadata.yaml" -editable_metadata = load_dict_from_file(editable_metadata_path) -metadata = dict_deep_update(metadata, editable_metadata) - -interface.run_conversion( - nwbfile_path="/Volumes/T9/Constantinople/nwbfiles/test.nwb", - metadata=metadata, - fiber_photometry_series_name="fiber_photometry_response_series_green", -) From 9b7bc8336155d14d536fe6adb348f899a80566a1 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 30 Sep 2024 14:29:58 +0200 Subject: [PATCH 06/29] wip --- .../fiber_photometry/fiber_photometry_convert_session.py | 2 +- .../metadata/fiber_photometry_metadata.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index 3650422..762cd5a 100644 --- 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 @@ -50,7 +50,7 @@ def session_to_nwb( dict( FiberPhotometry=dict( file_path=raw_fiber_photometry_file_path, - stream_name="/DataAcquisition/FPConsole/Signals/Series0001/AIN01xAOUT01-LockIn", + stream_name="/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn", ) ) ) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml index a882764..bd75023 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml @@ -130,10 +130,10 @@ Ophys: FiberPhotometryResponseSeries: - name: fiber_photometry_response_series_green description: The raw fluorescence signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn/" - stream_indices: [1, 3] + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn" + stream_indices: [1] #[1, 3] unit: a.u. - fiber_photometry_table_region: [0, 1] + fiber_photometry_table_region: [0] #[0, 1] fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw fluorescence signal. - name: fiber_photometry_response_series_red description: The raw fluorescence signal # TBD From bb3469c7aa43cc317bbdf690f8b4e9be3306336d Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sat, 5 Oct 2024 18:23:19 +0200 Subject: [PATCH 07/29] add interface for CSV doric format --- .../fiber_photometry/interfaces/__init__.py | 2 +- .../doric_fiber_photometry_interface.py | 71 ++++++++++++++++--- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py index 201cd5d..75e528e 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/interfaces/__init__.py @@ -1 +1 @@ -from .doric_fiber_photometry_interface import DoricFiberPhotometryInterface +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 index 635ec9e..17e4b5d 100644 --- 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 @@ -3,6 +3,7 @@ import h5py import numpy as np +import pandas as pd from ndx_fiber_photometry import FiberPhotometryResponseSeries from neuroconv import BaseTemporalAlignmentInterface from neuroconv.tools import get_module @@ -21,6 +22,7 @@ def __init__( verbose: bool = True, ): self._timestamps = None + self._time_column_name = "Time" super().__init__(file_path=file_path, stream_name=stream_name, verbose=verbose) def load(self, stream_name: str): @@ -30,13 +32,13 @@ def load(self, stream_name: str): raise ValueError(f"File '{file_path}' is not a .doric file.") channel_group = h5py.File(file_path, mode="r")[stream_name] - if "Time" not in channel_group.keys(): + if self._time_column_name not in channel_group.keys(): raise ValueError(f"Time not found in '{stream_name}'.") return channel_group def get_original_timestamps(self) -> np.ndarray: channel_group = self.load(stream_name=self.source_data["stream_name"]) - return channel_group["Time"][:] + return channel_group[self._time_column_name][:] def get_timestamps(self, stub_test: bool = False) -> np.ndarray: timestamps = self._timestamps if self._timestamps is not None else self.get_original_timestamps() @@ -47,6 +49,18 @@ def get_timestamps(self, stub_test: bool = False) -> np.ndarray: def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: self._timestamps = np.array(aligned_timestamps) + def _get_traces(self, stream_name: str, stream_indices: list, stub_test: bool = False): + traces_to_add = [] + data = self.load(stream_name=stream_name) + channel_names = list(data.keys()) + for stream_index in stream_indices: + trace = data[channel_names[stream_index]] + 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, @@ -93,15 +107,7 @@ def add_to_nwbfile( stream_name = trace_metadata["stream_name"] stream_indices = trace_metadata["stream_indices"] - traces_to_add = [] - data = self.load(stream_name=stream_name) - channel_names = list(data.keys()) - for stream_index in stream_indices: - trace = data[channel_names[stream_index]] - trace = trace[:100] if stub_test else trace[:] - traces_to_add.append(trace) - - traces = np.vstack(traces_to_add).T + traces = self._get_traces(stream_name=stream_name, stream_indices=stream_indices, stub_test=stub_test) fiber_photometry_table_region = fiber_photometry_table.create_fiber_photometry_table_region( description=trace_metadata["fiber_photometry_table_region_description"], @@ -129,3 +135,46 @@ def add_to_nwbfile( description="Contains the processed fiber photometry data.", ) ophys_module.add(fiber_photometry_response_series) + + +class DoricCsvFiberPhotometryInterface(DoricFiberPhotometryInterface): + + def __init__( + self, + file_path: Union[str, Path], + stream_name: str, + verbose: bool = True, + ): + super().__init__(file_path=file_path, stream_name=stream_name, verbose=verbose) + self._time_column_name = "Time(s)" + + def get_original_timestamps(self) -> np.ndarray: + channel_group = self.load(stream_name=self.source_data["stream_name"]) + return channel_group[self._time_column_name].values + + def load(self, stream_name: str): + 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=[0, 1]) + df = df.droplevel(0, axis=1) + if self._time_column_name not in df.columns: + raise ValueError(f"Time column not found in '{file_path}'.") + filtered_columns = [col for col in df.columns if stream_name in col] + filtered_columns.append(self._time_column_name) + df = df[filtered_columns] + return df + + def _get_traces(self, stream_name: str, stream_indices: list, stub_test: bool = False): + traces_to_add = [] + data = self.load(stream_name=stream_name) + channel_names = [col for col in data.columns if col != self._time_column_name] + for stream_index in stream_indices: + trace = data[channel_names[stream_index]] + trace = trace[:100] if stub_test else trace + traces_to_add.append(trace) + + traces = np.vstack(traces_to_add).T + return traces From 53cbe27640b72c25ec40155ea64409a21b8e4335 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sat, 5 Oct 2024 18:23:50 +0200 Subject: [PATCH 08/29] update convert session --- .../fiber_photometry_convert_session.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 index 762cd5a..c41a624 100644 --- 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 @@ -46,11 +46,21 @@ def session_to_nwb( session_id = session_id.replace("_", "-") # Add fiber photometry data + file_suffix = raw_fiber_photometry_file_path.suffix + if file_suffix == ".doric": + raw_stream_name = "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn" + elif file_suffix == ".csv": + raw_stream_name = "Raw" + else: + raise ValueError( + f"File '{raw_fiber_photometry_file_path}' extension should be either .doric or .csv and not '{file_suffix}'." + ) + source_data.update( dict( FiberPhotometry=dict( file_path=raw_fiber_photometry_file_path, - stream_name="/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn", + stream_name=raw_stream_name, ) ) ) From 779c1842e5a3df923c6ca41db3342dd6494e2215 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sat, 5 Oct 2024 18:24:15 +0200 Subject: [PATCH 09/29] conditional set data interface for fiber photometry based on file format --- .../fiber_photometry_nwbconverter.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) 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 index 3333024..0a62157 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py @@ -1,14 +1,28 @@ +from pathlib import Path + from neuroconv import NWBConverter from neuroconv.datainterfaces import DeepLabCutInterface, VideoInterface -from constantinople_lab_to_nwb.fiber_photometry.interfaces import DoricFiberPhotometryInterface +from constantinople_lab_to_nwb.fiber_photometry.interfaces import ( + DoricFiberPhotometryInterface, + DoricCsvFiberPhotometryInterface, +) class FiberPhotometryNWBConverter(NWBConverter): """Primary conversion class for converting the Fiber photometry dataset from the Constantinople Lab.""" data_interface_classes = dict( - FiberPhotometry=DoricFiberPhotometryInterface, DeepLabCut=DeepLabCutInterface, Video=VideoInterface, ) + + def __init__(self, source_data: dict[str, dict], verbose: bool = True): + """Validate source_data against source_schema and initialize all data interfaces.""" + fiber_photometry_source_data = source_data["FiberPhotometry"] + fiber_photometry_file_path = Path(fiber_photometry_source_data["file_path"]) + if fiber_photometry_file_path.suffix == ".doric": + self.data_interface_classes["FiberPhotometry"] = DoricFiberPhotometryInterface + elif fiber_photometry_file_path.suffix == ".csv": + self.data_interface_classes["FiberPhotometry"] = DoricCsvFiberPhotometryInterface + super().__init__(source_data=source_data, verbose=verbose) From 2e3d49bd7758eae12968f8eb909faa7c795c802f Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 9 Oct 2024 14:33:18 +0200 Subject: [PATCH 10/29] add behavior metadata example yaml --- .../metadata/behavior_metadata.yaml | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/metadata/behavior_metadata.yaml 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 From e9c11c107a1e285257796301f4aaf3c5815eea70 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 9 Oct 2024 14:33:40 +0200 Subject: [PATCH 11/29] add behavior interface --- .../fiber_photometry_convert_session.py | 31 +++++++++++++++++++ .../fiber_photometry_nwbconverter.py | 2 ++ 2 files changed, 33 insertions(+) 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 index c41a624..c227d69 100644 --- 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 @@ -13,6 +13,7 @@ def session_to_nwb( raw_fiber_photometry_file_path: Union[str, Path], + raw_behavior_file_path: Union[str, Path], nwbfile_path: Union[str, Path], dlc_file_path: Optional[Union[str, Path]] = None, video_file_path: Optional[Union[str, Path]] = None, @@ -26,6 +27,8 @@ def session_to_nwb( ---------- raw_fiber_photometry_file_path : Union[str, Path] Path to the raw fiber photometry file. + 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 @@ -79,6 +82,22 @@ def session_to_nwb( 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 @@ -100,6 +119,11 @@ def session_to_nwb( editable_metadata = load_dict_from_file(editable_metadata_path) metadata = dict_deep_update(metadata, editable_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) + # Run conversion converter.run_conversion( nwbfile_path=nwbfile_path, @@ -115,6 +139,12 @@ def session_to_nwb( doric_fiber_photometry_file_path = Path( "/Volumes/T9/Constantinople/Preprocessed_data/J069/Raw/J069_ACh_20230809_HJJ_0002.doric" ) + + # 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" @@ -133,6 +163,7 @@ def session_to_nwb( session_to_nwb( raw_fiber_photometry_file_path=doric_fiber_photometry_file_path, + raw_behavior_file_path=bpod_behavior_file_path, nwbfile_path=nwbfile_path, dlc_file_path=dlc_file_path, video_file_path=behavior_video_file_path, 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 index 0a62157..e94eadd 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py @@ -7,6 +7,7 @@ DoricFiberPhotometryInterface, DoricCsvFiberPhotometryInterface, ) +from constantinople_lab_to_nwb.general_interfaces import BpodBehaviorInterface class FiberPhotometryNWBConverter(NWBConverter): @@ -15,6 +16,7 @@ class FiberPhotometryNWBConverter(NWBConverter): data_interface_classes = dict( DeepLabCut=DeepLabCutInterface, Video=VideoInterface, + Behavior=BpodBehaviorInterface, ) def __init__(self, source_data: dict[str, dict], verbose: bool = True): From ff93588c8a367440b3e5558c94e48261f56f4aad Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 14 Nov 2024 16:26:59 +0100 Subject: [PATCH 12/29] add utility for adding the fiber response series to nwb --- .../fiber_photometry/utils/__init__.py | 6 +- .../utils/add_fiber_photometry.py | 82 ++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py b/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py index 4bee0ff..f724705 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/utils/__init__.py @@ -1 +1,5 @@ -from .add_fiber_photometry import add_fiber_photometry_devices, add_fiber_photometry_table +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 index 8ec8d6a..715b547 100644 --- 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 @@ -1,4 +1,8 @@ -from ndx_fiber_photometry import FiberPhotometryTable, FiberPhotometry +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 pynwb import NWBFile @@ -46,3 +50,79 @@ def add_fiber_photometry_table(nwbfile: NWBFile, metadata: dict): fiber_photometry_table=fiber_photometry_table, ) nwbfile.add_lab_meta_data(fiber_photometry_lab_meta_data) + + +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", +): + + add_fiber_photometry_devices(nwbfile=nwbfile, metadata=metadata) + + 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.") + + add_fiber_photometry_table(nwbfile=nwbfile, metadata=metadata) + fiber_photometry_table = nwbfile.lab_meta_data["FiberPhotometry"].fiber_photometry_table + + row_indices = trace_metadata["fiber_photometry_table_region"] + device_fields = [ + "optical_fiber", + "excitation_source", + "photodetector", + "dichroic_mirror", + "indicator", + "excitation_filter", + "emission_filter", + ] + for row_index in row_indices: + row_metadata = fiber_photometry_metadata["FiberPhotometryTable"]["rows"][row_index] + row_data = {field: nwbfile.devices[row_metadata[field]] for field in device_fields if field in row_metadata} + row_data["location"] = row_metadata["location"] + if "coordinates" in row_metadata: + row_data["coordinates"] = row_metadata["coordinates"] + if "commanded_voltage_series" in row_metadata: + row_data["commanded_voltage_series"] = nwbfile.acquisition[row_metadata["commanded_voltage_series"]] + fiber_photometry_table.add_row(**row_data) + + if traces.shape[1] != len(trace_metadata["fiber_photometry_table_region"]): + raise ValueError( + f"Number of channels ({traces.shape[1]}) should be equal to the number of rows referenced in the fiber photometry table ({len(trace_metadata['fiber_photometry_table_region'])})." + ) + + fiber_photometry_table_region = fiber_photometry_table.create_fiber_photometry_table_region( + description=trace_metadata["fiber_photometry_table_region_description"], + region=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=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) From f09bdf3c611c5c65b7cb68a50aafc7bc33e53e9b Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 14 Nov 2024 16:27:15 +0100 Subject: [PATCH 13/29] have separate metadata yamls for doric and csv format --- ... doric_csv_fiber_photometry_metadata.yaml} | 32 ++-- .../doric_fiber_photometry_metadata.yaml | 166 ++++++++++++++++++ 2 files changed, 184 insertions(+), 14 deletions(-) rename src/constantinople_lab_to_nwb/fiber_photometry/metadata/{fiber_photometry_metadata.yaml => doric_csv_fiber_photometry_metadata.yaml} (87%) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_fiber_photometry_metadata.yaml diff --git a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_csv_fiber_photometry_metadata.yaml similarity index 87% rename from src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml rename to src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_csv_fiber_photometry_metadata.yaml index bd75023..b00b1c7 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/metadata/fiber_photometry_metadata.yaml +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_csv_fiber_photometry_metadata.yaml @@ -85,7 +85,7 @@ Ophys: name: fiber_photometry_table description: TBD rows: - - name: 0 + - name: 1 location: DMS # coordinates: [0.8, 1.5, 2.8] # commanded_voltage_series: commanded_voltage_series_dms_calcium_signal @@ -96,7 +96,7 @@ Ophys: excitation_filter: excitation_filter emission_filter: emission_filter dichroic_mirror: dichroic_mirror - - name: 1 + - name: 3 location: DLS # coordinates: [0.8, 1.5, 2.8] indicator: grab_da_dms @@ -106,7 +106,7 @@ Ophys: excitation_filter: excitation_filter emission_filter: emission_filter dichroic_mirror: dichroic_mirror - - name: 2 + - name: 0 location: DMS # coordinates: [0.8, 1.5, 2.8] # commanded_voltage_series: commanded_voltage_series_dms_isosbestic_control @@ -117,7 +117,7 @@ Ophys: excitation_filter: isosbestic_excitation_filter emission_filter: emission_filter dichroic_mirror: dichroic_mirror - - name: 3 + - name: 2 location: DLS # coordinates: [0.8, 1.5, 2.8] indicator: mcherry @@ -128,17 +128,21 @@ Ophys: emission_filter: emission_filter dichroic_mirror: dichroic_mirror FiberPhotometryResponseSeries: - - name: fiber_photometry_response_series_green + - name: fiber_photometry_response_series description: The raw fluorescence signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn" - stream_indices: [1] #[1, 3] + channel_column_names: ["AIn-1 - Raw", "AIn-2 - Raw"] unit: a.u. - fiber_photometry_table_region: [0] #[0, 1] + 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_red - description: The raw fluorescence signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn/" - stream_indices: [0, 2] + - name: fiber_photometry_response_series_isosbestic + description: The isosbestic signal # TBD + channel_column_names: ["AIn-3"] unit: a.u. - fiber_photometry_table_region: [2, 3] - fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the raw fluorescence signal. + 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 # TBD + 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 motion corrected 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..948a3d6 --- /dev/null +++ b/src/constantinople_lab_to_nwb/fiber_photometry/metadata/doric_fiber_photometry_metadata.yaml @@ -0,0 +1,166 @@ +# 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: The optical fibers (Thor labs) with 400 μm core, 0.5 NA fiber optics were implanted over the injection site. + 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 + description: TBD + manufacturer: Doric Lenses + model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 0.0 # TBD + - name: excitation_source_isosbestic_control + description: TBD + manufacturer: Doric Lenses + model: TBD + illumination_type: LED + excitation_wavelength_in_nm: 0.0 # TBD + 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: 0.0 # TBD + # gain: # TBD + BandOpticalFilters: # Can't find any information in the referred papers + - name: emission_filter + description: TBD + manufacturer: Doric Lenses + model: TBD + center_wavelength_in_nm: 0.0 # TBD + bandwidth_in_nm: 0.0 # TBD + filter_type: Bandpass + - name: excitation_filter + description: TBD + manufacturer: Doric Lenses + model: TBD + center_wavelength_in_nm: 0.0 # TBD + bandwidth_in_nm: 0.0 # TBD + filter_type: Bandpass + - name: isosbestic_excitation_filter + description: TBD + manufacturer: Doric Lenses + model: TBD + center_wavelength_in_nm: 0.0 # TBD + bandwidth_in_nm: 0.0 # TBD + filter_type: Bandpass + DichroicMirrors: # Can't find any information in the referred papers + - name: dichroic_mirror + description: TBD + manufacturer: Doric Lenses + model: TBD + Indicators: + - name: grab_da_dms + description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the DMS (AP: -0.4 mm, ML: ±3.5 mm, DV: -4.6 mm from the skull surface at bregma at a 10 degrees angle)." + manufacturer: Addgene + label: AAV9-hSyn-GRAB + injection_location: DMS + injection_coordinates_in_mm: [-0.4, -3.5, -4.6] + - name: grab_ach_dms + description: "To measure acetylcholine activity, AAV9-hSyn-ACh4.3 (WZ Biosciences #YL10002) was injected into the DMS (AP: 1.7 mm, ML: ±2.8 mm, DV: -4.3 mm from the skull surface at bregma at a 10 degrees angle)." + manufacturer: WZ Biosciences + label: AAV9-hSyn-ACh4.3 + injection_location: DMS + injection_coordinates_in_mm: [1.7, 2.8, -4.3] + - name: grab_da_nacc + description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the NAcc (AP: 1.3 mm, ML: 1.65 mm, DV:-6.9 mm with an 8-10 degrees angle from the midline for bilateral implants)." + manufacturer: Addgene + label: AAV9-hSyn-GRAB + injection_location: NAcc + injection_coordinates_in_mm: [1.3, 1.65, -6.9] + - name: mcherry + description: "The control fluorophore was mCherry (AAV9-CB7-CI-mCherry-WPRE-RBG, AddGene #105544)." + label: AAV1-CB7-CI-mCherry + manufacturer: Addgene + FiberPhotometryTable: + name: fiber_photometry_table + description: TBD + rows: + - name: 1 + location: DMS + # coordinates: [0.8, 1.5, 2.8] + # commanded_voltage_series: commanded_voltage_series_dms_calcium_signal + indicator: grab_da_dms + optical_fiber: optical_fiber + excitation_source: excitation_source + photodetector: photodetector + excitation_filter: excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + - name: 3 + location: DLS + # coordinates: [0.8, 1.5, 2.8] + indicator: grab_da_dms + optical_fiber: optical_fiber + excitation_source: excitation_source + photodetector: photodetector + excitation_filter: excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + - name: 0 + location: DMS + # coordinates: [0.8, 1.5, 2.8] + # commanded_voltage_series: commanded_voltage_series_dms_isosbestic_control + indicator: mcherry + optical_fiber: optical_fiber + excitation_source: excitation_source_isosbestic_control + photodetector: photodetector + excitation_filter: isosbestic_excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + - name: 2 + location: DLS + # coordinates: [0.8, 1.5, 2.8] + indicator: mcherry + optical_fiber: optical_fiber + excitation_source: excitation_source_isosbestic_control + photodetector: photodetector + excitation_filter: isosbestic_excitation_filter + emission_filter: emission_filter + dichroic_mirror: dichroic_mirror + FiberPhotometryResponseSeries: + - name: fiber_photometry_response_series + description: The raw fluorescence signal # TBD + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn" + channel_ids: ["AIN01", "AIN02", "AIN03", "AIN04"] + unit: a.u. + 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_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT01" + channel_ids: ["AIN02", "AIN04"] + unit: a.u. + 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_green + description: The motion corrected signal # TBD + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT02"#, "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT03", "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT04"] + channel_ids: ["AIN02", "AIN04"] + unit: a.u. + fiber_photometry_table_region: [1, 3] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. + # TODO: modify to accept multiple streams + - name: fiber_photometry_response_series_motion_corrected_red_cordA + description: The motion corrected signal # TBD + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT03" #"/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT04"] + channel_ids: ["AIN01"] + unit: a.u. + fiber_photometry_table_region: [0] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. + - name: fiber_photometry_response_series_motion_corrected_red_cordB + description: The motion corrected signal # TBD + stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT04" + channel_ids: ["AIN03"] + unit: a.u. + fiber_photometry_table_region: [2] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. From 6dcf505c3591ddbe1e407329af93fbd11b370303 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 14 Nov 2024 16:28:44 +0100 Subject: [PATCH 14/29] simplify interfaces --- .../doric_fiber_photometry_interface.py | 202 +++++++++--------- 1 file changed, 99 insertions(+), 103 deletions(-) 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 index 17e4b5d..5cb36b8 100644 --- 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 @@ -1,15 +1,13 @@ from pathlib import Path -from typing import Union, Literal +from typing import Union import h5py import numpy as np import pandas as pd -from ndx_fiber_photometry import FiberPhotometryResponseSeries from neuroconv import BaseTemporalAlignmentInterface -from neuroconv.tools import get_module from pynwb import NWBFile -from constantinople_lab_to_nwb.fiber_photometry.utils import add_fiber_photometry_table, add_fiber_photometry_devices +from constantinople_lab_to_nwb.fiber_photometry.utils import add_fiber_photometry_response_series class DoricFiberPhotometryInterface(BaseTemporalAlignmentInterface): @@ -18,30 +16,31 @@ class DoricFiberPhotometryInterface(BaseTemporalAlignmentInterface): def __init__( self, file_path: Union[str, Path], - stream_name: str, + time_column_name: str = "Time", verbose: bool = True, ): + super().__init__(file_path=file_path, verbose=verbose) self._timestamps = None - self._time_column_name = "Time" - super().__init__(file_path=file_path, stream_name=stream_name, verbose=verbose) + self._time_column_name = time_column_name - def load(self, stream_name: str): + 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.") - channel_group = h5py.File(file_path, mode="r")[stream_name] - if self._time_column_name not in channel_group.keys(): - raise ValueError(f"Time not found in '{stream_name}'.") - return channel_group + return h5py.File(file_path, mode="r") - def get_original_timestamps(self) -> np.ndarray: - channel_group = self.load(stream_name=self.source_data["stream_name"]) + def get_original_timestamps(self, stream_name=str) -> np.ndarray: + channel_group = self.load()[stream_name] + 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, stub_test: bool = False) -> np.ndarray: - timestamps = self._timestamps if self._timestamps is not None else self.get_original_timestamps() + 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 @@ -49,12 +48,17 @@ def get_timestamps(self, stub_test: bool = False) -> np.ndarray: def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: self._timestamps = np.array(aligned_timestamps) - def _get_traces(self, stream_name: str, stream_indices: list, stub_test: bool = False): + def _get_traces(self, stream_name: str, channel_ids: list, stub_test: bool = False): traces_to_add = [] - data = self.load(stream_name=stream_name) - channel_names = list(data.keys()) - for stream_index in stream_indices: - trace = data[channel_names[stream_index]] + data = self.load() + if stream_name not in data: + raise ValueError(f"Stream '{stream_name}' not found in '{self.source_data['file_path']}'.") + channel_group = data[stream_name] + all_channel_names = list(channel_group.keys()) + for channel_name in channel_ids: + if channel_name not in all_channel_names: + raise ValueError(f"Channel '{channel_name}' not found in '{stream_name}'.") + trace = channel_group[channel_name] trace = trace[:100] if stub_test else trace[:] traces_to_add.append(trace) @@ -65,116 +69,108 @@ def add_to_nwbfile( self, nwbfile: NWBFile, metadata: dict, - fiber_photometry_series_name: str, - parent_container: Literal["acquisition", "processing/ophys"] = "acquisition", stub_test: bool = False, ) -> None: - add_fiber_photometry_devices(nwbfile=nwbfile, metadata=metadata) - 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.") - - add_fiber_photometry_table(nwbfile=nwbfile, metadata=metadata) - fiber_photometry_table = nwbfile.lab_meta_data["FiberPhotometry"].fiber_photometry_table - - row_indices = trace_metadata["fiber_photometry_table_region"] - device_fields = [ - "optical_fiber", - "excitation_source", - "photodetector", - "dichroic_mirror", - "indicator", - "excitation_filter", - "emission_filter", - ] - for row_index in row_indices: - row_metadata = fiber_photometry_metadata["FiberPhotometryTable"]["rows"][row_index] - row_data = {field: nwbfile.devices[row_metadata[field]] for field in device_fields if field in row_metadata} - row_data["location"] = row_metadata["location"] - if "coordinates" in row_metadata: - row_data["coordinates"] = row_metadata["coordinates"] - if "commanded_voltage_series" in row_metadata: - row_data["commanded_voltage_series"] = nwbfile.acquisition[row_metadata["commanded_voltage_series"]] - fiber_photometry_table.add_row(**row_data) - - stream_name = trace_metadata["stream_name"] - stream_indices = trace_metadata["stream_indices"] - - traces = self._get_traces(stream_name=stream_name, stream_indices=stream_indices, stub_test=stub_test) - - fiber_photometry_table_region = fiber_photometry_table.create_fiber_photometry_table_region( - description=trace_metadata["fiber_photometry_table_region_description"], - region=trace_metadata["fiber_photometry_table_region"], - ) - - # Get the timing information - timestamps = self.get_timestamps(stub_test=stub_test) - - fiber_photometry_response_series = FiberPhotometryResponseSeries( - name=trace_metadata["name"], - description=trace_metadata["description"], - 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.", + for trace_metadata in fiber_photometry_metadata["FiberPhotometryResponseSeries"]: + fiber_photometry_series_name = trace_metadata["name"] + stream_name = trace_metadata["stream_name"] + channel_ids = trace_metadata["channel_ids"] + + traces = self._get_traces(stream_name=stream_name, channel_ids=channel_ids, stub_test=stub_test) + # Get the timing information + timestamps = self.get_timestamps(stream_name=stream_name, stub_test=stub_test) + + parent_container = "processing/ophys" + if fiber_photometry_series_name == "fiber_photometry_response_series": + parent_container = "acquisition" + + add_fiber_photometry_response_series( + traces=traces, + timestamps=timestamps, + nwbfile=nwbfile, + metadata=metadata, + fiber_photometry_series_name=fiber_photometry_series_name, + parent_container=parent_container, ) - ophys_module.add(fiber_photometry_response_series) -class DoricCsvFiberPhotometryInterface(DoricFiberPhotometryInterface): +class DoricCsvFiberPhotometryInterface(BaseTemporalAlignmentInterface): def __init__( self, file_path: Union[str, Path], - stream_name: str, + time_column_name: str = "Time(s)", verbose: bool = True, ): - super().__init__(file_path=file_path, stream_name=stream_name, verbose=verbose) - self._time_column_name = "Time(s)" + 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: - channel_group = self.load(stream_name=self.source_data["stream_name"]) - return channel_group[self._time_column_name].values + df = self.load() + return df[self._time_column_name].values - def load(self, stream_name: str): + 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=[0, 1]) - df = df.droplevel(0, axis=1) + 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}'.") - filtered_columns = [col for col in df.columns if stream_name in col] - filtered_columns.append(self._time_column_name) - df = df[filtered_columns] return df - def _get_traces(self, stream_name: str, stream_indices: list, stub_test: bool = False): + def _get_traces(self, channel_column_names: list, stub_test: bool = False): traces_to_add = [] - data = self.load(stream_name=stream_name) - channel_names = [col for col in data.columns if col != self._time_column_name] - for stream_index in stream_indices: - trace = data[channel_names[stream_index]] + 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"] + + for trace_metadata in fiber_photometry_metadata["FiberPhotometryResponseSeries"]: + fiber_photometry_series_name = trace_metadata["name"] + channel_column_names = trace_metadata["channel_column_names"] + + parent_container = "processing/ophys" + if fiber_photometry_series_name == "fiber_photometry_response_series": + parent_container = "acquisition" + + 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=parent_container, + ) From 2baa12cf84ff25536575b73c470c5156a5710898 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 14 Nov 2024 16:29:20 +0100 Subject: [PATCH 15/29] update convert session --- .../fiber_photometry_convert_session.py | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) 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 index c41a624..7699452 100644 --- 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 @@ -25,7 +25,7 @@ def session_to_nwb( Parameters ---------- raw_fiber_photometry_file_path : Union[str, Path] - Path to the raw fiber photometry file. + Path to the raw fiber photometry file (.doric or .csv). nwbfile_path : Union[str, Path] Path to the NWB file. dlc_file_path : Union[str, Path], optional @@ -48,30 +48,18 @@ def session_to_nwb( # Add fiber photometry data file_suffix = raw_fiber_photometry_file_path.suffix if file_suffix == ".doric": - raw_stream_name = "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn" + fiber_photometry_metadata_file_name = "doric_fiber_photometry_metadata.yaml" + interface_name = "FiberPhotometryDoric" elif file_suffix == ".csv": - raw_stream_name = "Raw" + fiber_photometry_metadata_file_name = "doric_csv_fiber_photometry_metadata.yaml" + 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( - dict( - FiberPhotometry=dict( - file_path=raw_fiber_photometry_file_path, - stream_name=raw_stream_name, - ) - ) - ) - conversion_options.update( - dict( - FiberPhotometry=dict( - stub_test=stub_test, - fiber_photometry_series_name="fiber_photometry_response_series_green", - ) - ) - ) + 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))) @@ -96,7 +84,7 @@ def session_to_nwb( 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" / "fiber_photometry_metadata.yaml" + editable_metadata_path = Path(__file__).parent / "metadata" / fiber_photometry_metadata_file_name editable_metadata = load_dict_from_file(editable_metadata_path) metadata = dict_deep_update(metadata, editable_metadata) From 0368bd050b67ba5958d0a8523b061e441732adeb Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 14 Nov 2024 16:29:51 +0100 Subject: [PATCH 16/29] simplify converter --- .../fiber_photometry_nwbconverter.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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 index 0a62157..7739a81 100644 --- a/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py +++ b/src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_nwbconverter.py @@ -14,15 +14,7 @@ class FiberPhotometryNWBConverter(NWBConverter): data_interface_classes = dict( DeepLabCut=DeepLabCutInterface, + FiberPhotometryDoric=DoricFiberPhotometryInterface, + FiberPhotometryCsv=DoricCsvFiberPhotometryInterface, Video=VideoInterface, ) - - def __init__(self, source_data: dict[str, dict], verbose: bool = True): - """Validate source_data against source_schema and initialize all data interfaces.""" - fiber_photometry_source_data = source_data["FiberPhotometry"] - fiber_photometry_file_path = Path(fiber_photometry_source_data["file_path"]) - if fiber_photometry_file_path.suffix == ".doric": - self.data_interface_classes["FiberPhotometry"] = DoricFiberPhotometryInterface - elif fiber_photometry_file_path.suffix == ".csv": - self.data_interface_classes["FiberPhotometry"] = DoricCsvFiberPhotometryInterface - super().__init__(source_data=source_data, verbose=verbose) From b3a4143c8623e8fb5e28b887e4cdfafb017f5841 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Fri, 15 Nov 2024 13:31:22 +0100 Subject: [PATCH 17/29] Add docstrings to fiber photometry utility functions --- .../utils/add_fiber_photometry.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) 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 index 715b547..c490b65 100644 --- 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 @@ -8,6 +8,23 @@ 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 = [ @@ -30,6 +47,18 @@ def add_fiber_photometry_devices(nwbfile: NWBFile, metadata: dict): 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. + """ fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] fiber_photometry_table_metadata = fiber_photometry_metadata["FiberPhotometryTable"] @@ -60,7 +89,35 @@ def add_fiber_photometry_response_series( 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. + """ add_fiber_photometry_devices(nwbfile=nwbfile, metadata=metadata) fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] From 248453d5962473ef36f102c7b4e74c9387b06ab4 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 19 Nov 2024 15:53:18 +0100 Subject: [PATCH 18/29] skip adding rows that are already in the fiber photometry table --- .../fiber_photometry/utils/add_fiber_photometry.py | 2 ++ 1 file changed, 2 insertions(+) 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 index c490b65..93b5d1e 100644 --- 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 @@ -143,6 +143,8 @@ def add_fiber_photometry_response_series( "emission_filter", ] for row_index in row_indices: + if row_index in fiber_photometry_table.id: + continue row_metadata = fiber_photometry_metadata["FiberPhotometryTable"]["rows"][row_index] row_data = {field: nwbfile.devices[row_metadata[field]] for field in device_fields if field in row_metadata} row_data["location"] = row_metadata["location"] From a6e0524b55a5dde856f74c70176ec1a0769da814 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 19 Nov 2024 15:57:28 +0100 Subject: [PATCH 19/29] change metadata to stream_names in doric metadata to allow adding traces from different channel groups into the same series --- .../doric_fiber_photometry_metadata.yaml | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) 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 index 948a3d6..501de7c 100644 --- 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 @@ -130,37 +130,19 @@ Ophys: FiberPhotometryResponseSeries: - name: fiber_photometry_response_series description: The raw fluorescence signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/AnalogIn" - channel_ids: ["AIN01", "AIN02", "AIN03", "AIN04"] + stream_names: ["AnalogIn/AIN01", "AnalogIn/AIN02", "AnalogIn/AIN03", "AnalogIn/AIN04"] unit: a.u. 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_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT01" - channel_ids: ["AIN02", "AIN04"] + stream_names: ["LockInAOUT01/AIN02", "LockInAOUT01/AIN04"] unit: a.u. 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_green + - name: fiber_photometry_response_series_motion_corrected description: The motion corrected signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT02"#, "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT03", "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT04"] - channel_ids: ["AIN02", "AIN04"] + stream_names: ["LockInAOUT03/AIN01", "LockInAOUT04/AIN03", "LockInAOUT02/AIN02", "LockInAOUT02/AIN04"] unit: a.u. - fiber_photometry_table_region: [1, 3] - fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. - # TODO: modify to accept multiple streams - - name: fiber_photometry_response_series_motion_corrected_red_cordA - description: The motion corrected signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT03" #"/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT04"] - channel_ids: ["AIN01"] - unit: a.u. - fiber_photometry_table_region: [0] - fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. - - name: fiber_photometry_response_series_motion_corrected_red_cordB - description: The motion corrected signal # TBD - stream_name: "/DataAcquisition/FPConsole/Signals/Series0001/LockInAOUT04" - channel_ids: ["AIN03"] - unit: a.u. - fiber_photometry_table_region: [2] + fiber_photometry_table_region: [0, 1, 2, 3] fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. From 0ca8e081b96fe9310c148512089395bf4ece216a Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 20 Nov 2024 11:22:11 +0100 Subject: [PATCH 20/29] add notes --- .../fiber_photometry_notes.md | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/fiber_photometry_notes.md 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. +``` From 2e5a34cd8b2d8ae0455710a3c1e0d15874dd1984 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 20 Nov 2024 12:26:59 +0100 Subject: [PATCH 21/29] update interface --- .../doric_fiber_photometry_interface.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) 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 index 5cb36b8..ab40222 100644 --- 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 @@ -16,11 +16,17 @@ class DoricFiberPhotometryInterface(BaseTemporalAlignmentInterface): 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): @@ -31,8 +37,8 @@ def load(self): return h5py.File(file_path, mode="r") - def get_original_timestamps(self, stream_name=str) -> np.ndarray: - channel_group = self.load()[stream_name] + 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][:] @@ -48,17 +54,13 @@ def get_timestamps(self, stream_name=str, stub_test: bool = False) -> np.ndarray def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: self._timestamps = np.array(aligned_timestamps) - def _get_traces(self, stream_name: str, channel_ids: list, stub_test: bool = False): + def _get_traces(self, stream_names: list, stub_test: bool = False): traces_to_add = [] - data = self.load() - if stream_name not in data: - raise ValueError(f"Stream '{stream_name}' not found in '{self.source_data['file_path']}'.") - channel_group = data[stream_name] - all_channel_names = list(channel_group.keys()) - for channel_name in channel_ids: - if channel_name not in all_channel_names: - raise ValueError(f"Channel '{channel_name}' not found in '{stream_name}'.") - trace = channel_group[channel_name] + 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) @@ -75,12 +77,11 @@ def add_to_nwbfile( fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] for trace_metadata in fiber_photometry_metadata["FiberPhotometryResponseSeries"]: fiber_photometry_series_name = trace_metadata["name"] - stream_name = trace_metadata["stream_name"] - channel_ids = trace_metadata["channel_ids"] + stream_names = trace_metadata["stream_names"] - traces = self._get_traces(stream_name=stream_name, channel_ids=channel_ids, stub_test=stub_test) + traces = self._get_traces(stream_names=stream_names, stub_test=stub_test) # Get the timing information - timestamps = self.get_timestamps(stream_name=stream_name, stub_test=stub_test) + timestamps = self.get_timestamps(stream_name=stream_names[0], stub_test=stub_test) parent_container = "processing/ophys" if fiber_photometry_series_name == "fiber_photometry_response_series": From f4fe518eb29de6391416390c2f3a24d063b9f2bc Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 20 Nov 2024 12:27:27 +0100 Subject: [PATCH 22/29] try iterator --- .../fiber_photometry/utils/add_fiber_photometry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 93b5d1e..dc91f28 100644 --- 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 @@ -4,6 +4,7 @@ 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 @@ -170,7 +171,7 @@ def add_fiber_photometry_response_series( fiber_photometry_response_series = FiberPhotometryResponseSeries( name=trace_metadata["name"], description=trace_metadata["description"], - data=traces, + data=SliceableDataChunkIterator(data=traces), unit=trace_metadata["unit"], fiber_photometry_table_region=fiber_photometry_table_region, timestamps=timestamps, From 4b57f317c1c08effd101a1373f7d570029409315 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:04:28 +0100 Subject: [PATCH 23/29] update session_to_nwb --- .../fiber_photometry_convert_session.py | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) 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 index ce05281..8d14f3f 100644 --- 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 @@ -1,6 +1,4 @@ import os -import re -from datetime import datetime from pathlib import Path from typing import Union, Optional @@ -10,10 +8,14 @@ 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, @@ -27,6 +29,10 @@ def session_to_nwb( ---------- 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] @@ -44,17 +50,16 @@ def session_to_nwb( conversion_options = dict() raw_fiber_photometry_file_path = Path(raw_fiber_photometry_file_path) - raw_fiber_photometry_file_name = raw_fiber_photometry_file_path.stem - subject_id, session_id = raw_fiber_photometry_file_name.split("_", maxsplit=1) + + 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": - fiber_photometry_metadata_file_name = "doric_fiber_photometry_metadata.yaml" interface_name = "FiberPhotometryDoric" elif file_suffix == ".csv": - fiber_photometry_metadata_file_name = "doric_csv_fiber_photometry_metadata.yaml" interface_name = "FiberPhotometryCsv" else: raise ValueError( @@ -90,28 +95,26 @@ def session_to_nwb( # Add datetime to conversion metadata = converter.get_metadata() - metadata["NWBFile"].update(session_id=session_id) + metadata["NWBFile"].update(session_id=session_id, protocol=protocol) - date_pattern = r"(?P\d{8})" - - match = re.search(date_pattern, raw_fiber_photometry_file_name) - if match: - date_str = match.group("date") - date_obj = datetime.strptime(date_str, "%Y%m%d") - session_start_time = date_obj - tzinfo = tz.gettz("America/New_York") - metadata["NWBFile"].update(session_start_time=session_start_time.replace(tzinfo=tzinfo)) + 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" / fiber_photometry_metadata_file_name + 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, @@ -127,6 +130,9 @@ def session_to_nwb( 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( @@ -142,16 +148,26 @@ def session_to_nwb( "/Volumes/T9/Constantinople/Compressed Videos/J069/J069-2023-08-09_rig104cam01_0002comp.mp4" ) # NWB file path - nwbfile_path = Path("/Volumes/T9/Constantinople/nwbfiles/J069_ACh_20230809_HJJ_0002.nwb") + 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, From 582830607378a7c5b98fe5ce54407d1937d9d5ab Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:04:49 +0100 Subject: [PATCH 24/29] update metadata yaml --- .../doric_csv_fiber_photometry_metadata.yaml | 145 ++++++----------- .../doric_fiber_photometry_metadata.yaml | 154 +++++++----------- 2 files changed, 107 insertions(+), 192 deletions(-) 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 index b00b1c7..7457e98 100644 --- 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 @@ -5,144 +5,93 @@ Ophys: FiberPhotometry: OpticalFibers: - name: optical_fiber - description: The optical fibers (Thor labs) with 400 μm core, 0.5 NA fiber optics were implanted over the injection site. + 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 - description: TBD + - name: excitation_source_grab_da + description: The excitation wavelength for GRAB-DA indicator. manufacturer: Doric Lenses - model: TBD + # model: TBD illumination_type: LED - excitation_wavelength_in_nm: 0.0 # TBD - - name: excitation_source_isosbestic_control - description: TBD + excitation_wavelength_in_nm: 470.0 + - name: excitation_source_mcherry + description: The excitation wavelength for mCherry indicator. manufacturer: Doric Lenses - model: TBD + # model: TBD illumination_type: LED - excitation_wavelength_in_nm: 0.0 # TBD + 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 + # description: TBD manufacturer: Doric Lenses - model: TBD + # model: TBD detector_type: photodiode - detected_wavelength_in_nm: 0.0 # TBD + detected_wavelength_in_nm: 470.0 # TBD # gain: # TBD - BandOpticalFilters: # Can't find any information in the referred papers - - name: emission_filter - description: TBD - manufacturer: Doric Lenses - model: TBD - center_wavelength_in_nm: 0.0 # TBD - bandwidth_in_nm: 0.0 # TBD - filter_type: Bandpass - - name: excitation_filter - description: TBD - manufacturer: Doric Lenses - model: TBD - center_wavelength_in_nm: 0.0 # TBD - bandwidth_in_nm: 0.0 # TBD - filter_type: Bandpass - - name: isosbestic_excitation_filter - description: TBD - manufacturer: Doric Lenses - model: TBD - center_wavelength_in_nm: 0.0 # TBD - bandwidth_in_nm: 0.0 # TBD - filter_type: Bandpass DichroicMirrors: # Can't find any information in the referred papers - name: dichroic_mirror - description: TBD + # description: TBD manufacturer: Doric Lenses - model: TBD + # model: TBD Indicators: - - name: grab_da_dms - description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the DMS (AP: -0.4 mm, ML: ±3.5 mm, DV: -4.6 mm from the skull surface at bregma at a 10 degrees angle)." + - 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 - injection_location: DMS - injection_coordinates_in_mm: [-0.4, -3.5, -4.6] - - name: grab_ach_dms - description: "To measure acetylcholine activity, AAV9-hSyn-ACh4.3 (WZ Biosciences #YL10002) was injected into the DMS (AP: 1.7 mm, ML: ±2.8 mm, DV: -4.3 mm from the skull surface at bregma at a 10 degrees angle)." - manufacturer: WZ Biosciences - label: AAV9-hSyn-ACh4.3 - injection_location: DMS - injection_coordinates_in_mm: [1.7, 2.8, -4.3] - - name: grab_da_nacc - description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the NAcc (AP: 1.3 mm, ML: 1.65 mm, DV:-6.9 mm with an 8-10 degrees angle from the midline for bilateral implants)." - manufacturer: Addgene - label: AAV9-hSyn-GRAB + label: AAV9-hsyn-GRAB-DA2h injection_location: NAcc - injection_coordinates_in_mm: [1.3, 1.65, -6.9] + # injection_coordinates_in_mm: [-0.4, -3.5, -4.6] - name: mcherry - description: "The control fluorophore was mCherry (AAV9-CB7-CI-mCherry-WPRE-RBG, AddGene #105544)." + 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: TBD + description: The metadata of the fiber photometry setup. rows: - - name: 1 - location: DMS - # coordinates: [0.8, 1.5, 2.8] - # commanded_voltage_series: commanded_voltage_series_dms_calcium_signal - indicator: grab_da_dms - optical_fiber: optical_fiber - excitation_source: excitation_source - photodetector: photodetector - excitation_filter: excitation_filter - emission_filter: emission_filter - dichroic_mirror: dichroic_mirror - - name: 3 - location: DLS + - name: 0 + location: NAcc # coordinates: [0.8, 1.5, 2.8] - indicator: grab_da_dms + indicator: mcherry optical_fiber: optical_fiber - excitation_source: excitation_source + excitation_source: excitation_source_mcherry photodetector: photodetector - excitation_filter: excitation_filter - emission_filter: emission_filter dichroic_mirror: dichroic_mirror - - name: 0 - location: DMS + - name: 1 + location: NAcc # coordinates: [0.8, 1.5, 2.8] - # commanded_voltage_series: commanded_voltage_series_dms_isosbestic_control - indicator: mcherry + indicator: grab_da optical_fiber: optical_fiber - excitation_source: excitation_source_isosbestic_control + excitation_source: excitation_source_grab photodetector: photodetector - excitation_filter: isosbestic_excitation_filter - emission_filter: emission_filter dichroic_mirror: dichroic_mirror - name: 2 - location: DLS - # coordinates: [0.8, 1.5, 2.8] - indicator: mcherry + location: NAcc + # coordinates: [0.8, 1.5, 2.8] + indicator: grab_da optical_fiber: optical_fiber - excitation_source: excitation_source_isosbestic_control + excitation_source: excitation_source_grab_isosbestic photodetector: photodetector - excitation_filter: isosbestic_excitation_filter - emission_filter: emission_filter dichroic_mirror: dichroic_mirror FiberPhotometryResponseSeries: - - name: fiber_photometry_response_series - description: The raw fluorescence signal # TBD - channel_column_names: ["AIn-1 - Raw", "AIn-2 - Raw"] - unit: a.u. - 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 # TBD - channel_column_names: ["AIn-3"] + - 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] - 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 + 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 motion corrected signal. + 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 index 501de7c..a69357f 100644 --- 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 @@ -5,144 +5,110 @@ Ophys: FiberPhotometry: OpticalFibers: - name: optical_fiber - description: The optical fibers (Thor labs) with 400 μm core, 0.5 NA fiber optics were implanted over the injection site. + 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 - description: TBD + - name: excitation_source_grab_da + description: The excitation wavelength for GRAB-DA indicator. manufacturer: Doric Lenses - model: TBD + # model: TBD illumination_type: LED - excitation_wavelength_in_nm: 0.0 # TBD - - name: excitation_source_isosbestic_control - description: TBD + excitation_wavelength_in_nm: 470.0 + - name: excitation_source_grab_ach + description: The excitation wavelength for GRAB-ACh indicator. manufacturer: Doric Lenses - model: TBD + # model: TBD illumination_type: LED - excitation_wavelength_in_nm: 0.0 # TBD + 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 + # description: TBD manufacturer: Doric Lenses - model: TBD + # model: TBD detector_type: photodiode - detected_wavelength_in_nm: 0.0 # TBD + detected_wavelength_in_nm: 470.0 # TBD # gain: # TBD - BandOpticalFilters: # Can't find any information in the referred papers - - name: emission_filter - description: TBD - manufacturer: Doric Lenses - model: TBD - center_wavelength_in_nm: 0.0 # TBD - bandwidth_in_nm: 0.0 # TBD - filter_type: Bandpass - - name: excitation_filter - description: TBD - manufacturer: Doric Lenses - model: TBD - center_wavelength_in_nm: 0.0 # TBD - bandwidth_in_nm: 0.0 # TBD - filter_type: Bandpass - - name: isosbestic_excitation_filter - description: TBD - manufacturer: Doric Lenses - model: TBD - center_wavelength_in_nm: 0.0 # TBD - bandwidth_in_nm: 0.0 # TBD - filter_type: Bandpass DichroicMirrors: # Can't find any information in the referred papers - name: dichroic_mirror - description: TBD + # description: TBD manufacturer: Doric Lenses - model: TBD + # model: TBD Indicators: - - name: grab_da_dms - description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the DMS (AP: -0.4 mm, ML: ±3.5 mm, DV: -4.6 mm from the skull surface at bregma at a 10 degrees angle)." + - 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 + label: AAV9-hsyn-GRAB-DA2h injection_location: DMS injection_coordinates_in_mm: [-0.4, -3.5, -4.6] - - name: grab_ach_dms - description: "To measure acetylcholine activity, AAV9-hSyn-ACh4.3 (WZ Biosciences #YL10002) was injected into the DMS (AP: 1.7 mm, ML: ±2.8 mm, DV: -4.3 mm from the skull surface at bregma at a 10 degrees angle)." + - 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: grab_da_nacc - description: "To measure dopamine activity, AAV9-hsyn-GRAB DA2h (AddGene #140554) was injected into the NAcc (AP: 1.3 mm, ML: 1.65 mm, DV:-6.9 mm with an 8-10 degrees angle from the midline for bilateral implants)." - manufacturer: Addgene - label: AAV9-hSyn-GRAB - injection_location: NAcc - injection_coordinates_in_mm: [1.3, 1.65, -6.9] + injection_coordinates_in_mm: [1.7, -2.8, -4.3] - name: mcherry - description: "The control fluorophore was mCherry (AAV9-CB7-CI-mCherry-WPRE-RBG, AddGene #105544)." + 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: TBD + description: The metadata of the fiber photometry setup. rows: - - name: 1 + - name: 0 location: DMS # coordinates: [0.8, 1.5, 2.8] - # commanded_voltage_series: commanded_voltage_series_dms_calcium_signal - indicator: grab_da_dms - optical_fiber: optical_fiber - excitation_source: excitation_source - photodetector: photodetector - excitation_filter: excitation_filter - emission_filter: emission_filter - dichroic_mirror: dichroic_mirror - - name: 3 - location: DLS - # coordinates: [0.8, 1.5, 2.8] - indicator: grab_da_dms + indicator: mcherry optical_fiber: optical_fiber - excitation_source: excitation_source + excitation_source: excitation_source_mcherry photodetector: photodetector - excitation_filter: excitation_filter - emission_filter: emission_filter dichroic_mirror: dichroic_mirror - - name: 0 + - name: 1 location: DMS # coordinates: [0.8, 1.5, 2.8] - # commanded_voltage_series: commanded_voltage_series_dms_isosbestic_control - indicator: mcherry + indicator: grab_ach optical_fiber: optical_fiber - excitation_source: excitation_source_isosbestic_control + excitation_source: excitation_source_grab_ach photodetector: photodetector - excitation_filter: isosbestic_excitation_filter - emission_filter: emission_filter dichroic_mirror: dichroic_mirror - name: 2 - location: DLS - # coordinates: [0.8, 1.5, 2.8] - indicator: mcherry + location: DMS + indicator: grab_ach optical_fiber: optical_fiber - excitation_source: excitation_source_isosbestic_control + excitation_source: excitation_source_grab_ach_isosbestic photodetector: photodetector - excitation_filter: isosbestic_excitation_filter - emission_filter: emission_filter dichroic_mirror: dichroic_mirror FiberPhotometryResponseSeries: - - name: fiber_photometry_response_series - description: The raw fluorescence signal # TBD - stream_names: ["AnalogIn/AIN01", "AnalogIn/AIN02", "AnalogIn/AIN03", "AnalogIn/AIN04"] - unit: a.u. - 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"] + - 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, 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] + 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, 2, 3] - fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the motion corrected signal. + fiber_photometry_table_region: [0, 1, 0, 2] + fiber_photometry_table_region_description: The region of the FiberPhotometryTable corresponding to the estimated signal. From 867e8f17f39dceb33843942dbfc677736ff376ae Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:05:08 +0100 Subject: [PATCH 25/29] update writing always to acquisition --- .../doric_fiber_photometry_interface.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 index ab40222..dee3c5b 100644 --- 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 @@ -7,7 +7,10 @@ from neuroconv import BaseTemporalAlignmentInterface from pynwb import NWBFile -from constantinople_lab_to_nwb.fiber_photometry.utils import add_fiber_photometry_response_series +from constantinople_lab_to_nwb.fiber_photometry.utils import ( + add_fiber_photometry_response_series, + add_fiber_photometry_table, +) class DoricFiberPhotometryInterface(BaseTemporalAlignmentInterface): @@ -75,6 +78,8 @@ def add_to_nwbfile( ) -> 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"] @@ -83,17 +88,13 @@ def add_to_nwbfile( # Get the timing information timestamps = self.get_timestamps(stream_name=stream_names[0], stub_test=stub_test) - parent_container = "processing/ophys" - if fiber_photometry_series_name == "fiber_photometry_response_series": - parent_container = "acquisition" - add_fiber_photometry_response_series( traces=traces, timestamps=timestamps, nwbfile=nwbfile, metadata=metadata, fiber_photometry_series_name=fiber_photometry_series_name, - parent_container=parent_container, + parent_container="acquisition", ) @@ -159,19 +160,17 @@ def add_to_nwbfile( 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"] - parent_container = "processing/ophys" - if fiber_photometry_series_name == "fiber_photometry_response_series": - parent_container = "acquisition" - 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=parent_container, + parent_container="acquisition", ) From cd056483b2db1630c67947f830537aeef471811e Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:05:45 +0100 Subject: [PATCH 26/29] move populating rows in the fiberphotometry table to its own function --- .../utils/add_fiber_photometry.py | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) 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 index dc91f28..c948360 100644 --- 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 @@ -60,6 +60,8 @@ def add_fiber_photometry_table(nwbfile: NWBFile, 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"] @@ -70,10 +72,6 @@ def add_fiber_photometry_table(nwbfile: NWBFile, metadata: dict): name=fiber_photometry_table_metadata["name"], description=fiber_photometry_table_metadata["description"], ) - # fiber_photometry_table.add_column( - # name="additional_column_name", - # description="additional_column_description", - # ) fiber_photometry_lab_meta_data = FiberPhotometry( name="FiberPhotometry", @@ -81,6 +79,24 @@ def add_fiber_photometry_table(nwbfile: NWBFile, metadata: dict): ) 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, @@ -119,8 +135,6 @@ def add_fiber_photometry_response_series( 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. """ - add_fiber_photometry_devices(nwbfile=nwbfile, metadata=metadata) - fiber_photometry_metadata = metadata["Ophys"]["FiberPhotometry"] traces_metadata = fiber_photometry_metadata["FiberPhotometryResponseSeries"] trace_metadata = next( @@ -130,39 +144,12 @@ def add_fiber_photometry_response_series( if trace_metadata is None: raise ValueError(f"Trace metadata for '{fiber_photometry_series_name}' not found.") - add_fiber_photometry_table(nwbfile=nwbfile, metadata=metadata) fiber_photometry_table = nwbfile.lab_meta_data["FiberPhotometry"].fiber_photometry_table - - row_indices = trace_metadata["fiber_photometry_table_region"] - device_fields = [ - "optical_fiber", - "excitation_source", - "photodetector", - "dichroic_mirror", - "indicator", - "excitation_filter", - "emission_filter", - ] - for row_index in row_indices: - if row_index in fiber_photometry_table.id: - continue - row_metadata = fiber_photometry_metadata["FiberPhotometryTable"]["rows"][row_index] - row_data = {field: nwbfile.devices[row_metadata[field]] for field in device_fields if field in row_metadata} - row_data["location"] = row_metadata["location"] - if "coordinates" in row_metadata: - row_data["coordinates"] = row_metadata["coordinates"] - if "commanded_voltage_series" in row_metadata: - row_data["commanded_voltage_series"] = nwbfile.acquisition[row_metadata["commanded_voltage_series"]] - fiber_photometry_table.add_row(**row_data) - - if traces.shape[1] != len(trace_metadata["fiber_photometry_table_region"]): - raise ValueError( - f"Number of channels ({traces.shape[1]}) should be equal to the number of rows referenced in the fiber photometry table ({len(trace_metadata['fiber_photometry_table_region'])})." - ) + 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=trace_metadata["fiber_photometry_table_region"], + region=list(trace_metadata["fiber_photometry_table_region"]), ) if traces.shape[0] != len(timestamps): From aa3388c4b458ae5ff51b153ddadd5041eec6a382 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:06:12 +0100 Subject: [PATCH 27/29] add convert_all_sessions.py --- .../fiber_photometry/convert_all_sessions.py | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/convert_all_sessions.py 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, + ) From d17a5988ea1200b0449a920410bb659b51b67d4a Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:09:07 +0100 Subject: [PATCH 28/29] update general_metadata.yaml --- .../metadata/general_metadata.yaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/metadata/general_metadata.yaml 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 From 7777dc4468d5e6ea73711fc29a88cf219519539c Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 5 Dec 2024 14:43:54 +0100 Subject: [PATCH 29/29] add sessions_table template --- .../fiber_photometry/all_sessions_table.xlsx | Bin 0 -> 12358 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/constantinople_lab_to_nwb/fiber_photometry/all_sessions_table.xlsx 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 0000000000000000000000000000000000000000..c98aed19a8ee5a508b67746c015e4a3742fd9be3 GIT binary patch literal 12358 zcmeHtWkXy`)^+2q!6mph4#C~6ad&rjf+V=Ry99^e?(PH+E7VSW>uKh4Xo!th<3#E4X zoC}^f5}M+qD`e>!FpTZ#t9gf3xR1EzJal3pjFZCtvS4w;ZxEd8X?Kg%TML{iLaGSw zV7T_L>`ZqF{XS)CAu;3e_sY`Ko61{1w^M`}D4L+Gbbl?!poO%i6_ce^mwRyk1B*3Raw-K`j;+>926iHoo+#_6f_;W*ju7RN-OoMI0c#vw}!WTe5@S0y8< z=XRqCSyq6R)QD+Ch~qZx@o(n%D;5=gFpRzSmbw*2Lg9@s0dJ1@B$ztK6Lm# zj;^fnMjZ~5TyL>eM4)5vkTtr04oP%hcynKaB{XCw2aO7CGClB+vBRb>k<%W9)+CvH+teG{AR3(<_OH&5R5a+$+Q zq-^N7ERzyL6uDO)wCdT;#UO8j7UHL0C)gc zFb^BXfAGZJ-pSg?-ro9G4Eq;nz+MB{tC#=o)*e4$)z5?&cpB6hFx}x8|Ew^|?L`yR z(YOM`RD&J@vN0|d@O(&5JYlacNSXRX&UV2(ZuY=+xkX6kx=uq~O90urobjarUhhU; z{f0QSfwlDp88cF{`ZEI~`{bgvoV-b!AZaP5l0jNuK%$B^0h=j*u1w0{kdQN;F)*MK zrBzJx`(0LK*Sp+w%kV5kRQKVF(Gi(e$zr9rLLM`d&)0{%sD`7@P!>zX7%zAEhV`jS8U_{sXv;pZ`*uu60umn(bbfX zrL@^%ke#ciaUfj>qrHV_Wdq9@V<~y2p2mU#kG(M`wYDqs>2Byid^jWt6$J~xW@VG2 z)fBc}(r)rjl`7v9N#m>VSCu}cb)K@MNUSmw*@dKSBLePsyHOn?k~_>eMjhX|GQ!%+ zH9xr%$E&J9_^6(%Az8TkBI1Y$?1& zZqX0}crqc2P$cGgQ-s4B_`xj9CGJHYA$bDZrFyQ7k)uV<2}j`y4${?+-KcaU8h@y3Wn`4ul;=yELoHO`tdMDhBYD-4 zw6Xv2fKu}(6SmnqCf);DV%mV(q!|T=A$U214UeG*-vlz3vLztR+tA`6ajy07R@KjARQGrIF&$C zYfa!!t0Aho1l0>pnC()hb=5KSw9Xk>R#wau|Iei1?IBF7mo|KIBD3GJu?YcfC(X11zP*e5f?CiB(J7OxFJZW+m)Pn&-eQ^e9 z+V8HIiH;qmG#RmFfK{mYNa!DD7>}@g;8cYV!Pn@6Is!9Gv4)K0pAe1B$d-Nf95DW9 zhhwt=>MjylcWP8*glCnp_8Q6%wi#>;@j+DNLpov#oG;>7KNC(d=IK^+S$v2%dqBr& z`?P2yOo3}={D2FyWD?Gbjg}b|%W+1=mKKPHH(_j#QJ}~~1jR?ThgsN(4bkA+nBM7Y zm&)P;iD%ihQoPh?Vk8&0C#FwV^;DdHcxX1L!r-~m^i5q}IDwb<;2In!dfYNew!tj;Y643CS%q#$XRg;=(Wk82T zoeAzQ&*1zV#Mt8~0l(GdoLOKCzs(*fN`&{&|NLe5;W%MO6A0C6Ui3|?khA$Gvr*X; zg?a`?SI1Zfu6R^oS%EMxd5h2bXWj8ZLP3pJk6lxwCM0T^)Y;eN_d=4&HfaSDL6vF# z97*i)TB+$smR5ZhOU)pq*r^+1QX2JU360?`cR})Zr|u2v7MjXq3px&Mr`1gH16z0E zmLdz_!nOf4^a}X>l%k)xI1ef|?&8{+TBnmb06O{Q;n>Fy4tpkY7LLN;J3}N;XlH$O ztJ)Wm1vJdZ**y0S$tqe)(3tOwZNiHKNMzFG_p{-1Ir_6H8a57i(L3f^a$- zt~Cp>pifWL#yEo9P;a)r>cXFvu^u-HTTwY%=A_UB9}U2LX9^SzHjG)2CwRvBJoL6_ z5cNE#FTas{>t(?1(y*A`%UEMz<=f*UEPbF2k=z*$LcS~gHP_q@wnn8>SCA{kt+>=z z2s1c>ec$lL<)8AK!)uVy7LojTS{pUjo9XrQF-P2YRZgDN=W?vM@o^ZFfgXt%PB*&Y z`fXpxH`$3y{J!!q(5a(@qB4zLlUr|O(H^zRQr&*S-YhY}T!Zs+_fFfIy~PCH$@$X; ztJ>doG!lx2%WtQgTtd}cfUUOMY`zboTE@-Zn&cQ8yE~*r=}7C!^qhr@X!oqw@tnSV z-=ekaEO1%X75RQm-=($kT>!EFK|b0P%0%oj>f?`*QmVZHm%^joK*(0{{#X|EeGS5kp-pOl?gW|F|;$BG|F!LHCC4RcKkQs6kJzI)CGkp!uEWLGlCFE9hWh1jz9o;!+*Z?b*gU$~sjbfmtQNJZ$4PuA~LIOofZTuddOP5xlb zNzLXhFPRvLW(sF_hd}ozvy$WMgl0PeJG2OKCu)N4XiX8j+!vM>Sp#p4 zY3K8U9;+E**Z?iBLVbu5ouR9vC#|l=nPM;tHvgoe#zpR~+qu;C}1_*V;(@ZMHB9$L2Da&e*a_nkFp0Mx(K7uQeOKpt|Aj(g{ z);32_b8YsoP|U)j+Xtgl8Z@iWAW5s2rR5kbu46bG0WQL%6&`K$!ZIB&_CVrUHBhU@k#0Ed z+?!?TV?Td7WXrB4T(pF>g-yj-pD;*Q0B-hJbJe$$mZGXnzaxdChZS-5%qVx&h(r>W zw~2)AF7UBz$ySG5o}g9*gw9fkI2lkVG1Q9~g^u-_5(h4yPpsQk^_5u9^Ld+)8NQB# z$v3ogcW}&tLGf6LOO*K?VP}NN4vW)=9tp}yMsm^j;WQWb#&m%)-?>As@PxYH5x0>a z-s<+V$ICeF&$L9~u*UJZEk-HuIxow{5BK|0B9n;4xd%vLd&eyCi1LXLDAY2qHo-Oa zKADj=L+x0lCsKvpn|>jzX^%|nAi{R>uZ}QX=51VTmIwR`)GQl6ggXUn(O?6Q(1o{T{avZy-k0YO1GLwM!)SyDL}=t95aQeC1SePof$5BrQ*R#nQL4S;tIsfnfm-7ci|Be zH7jS{Qqh`TiU1XV=Ruy_6S;K~kF@|Q(UQMrGrJ3pJTtZbrt92JFBD;7|t5kauuXIlFqulA6;#qH-mBa<9+Y& zfwNd?UqRSDg^c=s27fq_-5_E$K{E<=qn(S2{>7KZ24+oqkvTIbM}7G={QPEiMW_UIl=$8bu_&9Z7}f#GC(mwqA$Eh0ize;m8v+x=Fj&XTv| zDH?IsL&IpUrmdS*kXZvCHDrg}r>Wzw+(CMuwNH`WuT@VI@c!%|LqcAjw^KOcfNyYW z(uTF7F!lYUSpt(J%~R5Z|CIYg3^X>_rSUQ81l(A)HNO09@gpwYfhQOm0MH?LE#dzo z{QgQ|oh?jFU7Q*J=>CYonThuH%S@<)8}iHUp^w?*Zoos?|El9Xhgd_k_@Wa}6+fbSwktrIlqirCqIcx|syvN9#bYceX{qqkyCUL)- zdYx(EB=6wt7NG@o`wdwXX@PY5J$8VwrJ05G4?A?RVB3}gN);ZxUPkLeiiV%6_M~;Q znU!Vanxf>8b^c=?_}^!pszHLTf`nnale_l{loOcd@P zHNu4#_u#|9M<{PHlq_56HdiGwm9PljwZpc*(JxvSC?|)S9W7}<@*|i}pNz?o@D$aQ z;THJ7h$u%kkrRk-5JFm&5Ew!q;ZnVmR9z9&;&w2iS7Ek4IpNKpIb_KziM3?$ap=G! zc7x$3u{`$08EVY=?e%xY*7Qn9*~Ki{DQ`Xz4v%xFHyFAgAjA{uqk-I3_slK?BWE2! zxB-Sn!$bFi+m{HV;PswC`@$WFPj;!CP#H}d0$dTjIc2)$M}a1v9)j}jHk z2HsSq5itu+R7qz>2x-nq^F(!ww;j@Yf9;9K<5wS$uc_6hM$l682HgZbl_8(DEZWc? zn;{RYfGz#Rl>}ROvrK(ZqlMzi(qkAO%h_WR3f{<}CKY3Qel%V`64)>zD`A*i=y;tY z+O0-mconu8Nx^X~$C<{cHj!?kv9jZ^Dsfse5;$*8*>TysT{eSYoe(;k5YHF1hXnOC z8k~MV?L=Iw)IkWQyK}>bd1?(0LMULwVIjB^2S+2yFZTk(^VErR%8%$rB<WAff=lsLChu5_3<>eLLFE4?q zx^^o}sDV}Jx8Pq<#KwY1a~rqoL5YhJO#T2LtAIs^O8ps?!Yoat_trOk@ndNepFz&g zPtU$fSq#OEcaIOQH=bR?*zS8VvpIDS3r*e)78x8wPba-84+Be2FUw{V=gaJ4P1&~O zV{x_=zy(-$fOQ}W02t`?k)l@uj^+$zPWtLGu{u}z8C-J6JAMkMVh{H)&y;7lz*L`{ z+($3(?dlm|oRk+_2g%_8&tTu5bRCL;*99!gX8?Gjyx*(kh z5dCiBy)%&;dwyelxL2U}+%{YpEvg&3mOtabkLt#vPwm0YBPDmp)i^Drn#}K!Dia;m zbvO^g<%8>cXoP)V{G1(K zo5=?CWIdfaf&8X}n3Cq|f~uYWG6jraj?gu@!^GyWBs2p_Do^3*#NEYe=X6WADw`e7 zD{m8V)rqro;}q5~KORdjdd^@DQ>l~J%^?#Cx)n;VCZR64i!ltkL($5vV&c0G1SR}w z(E=W;3&$hh3Vyc}Zj$`<1W*fP|0W%J)JBSJte)E&2rO>~GGw|86puH99~_4Vj{e&( zTr#)=mM~wKy=||md_t|&4Vd5ekjr!!xUOPiztt%%83aAOTmUC&YfgUfSR$mkT=EG% z=O;52!+h5i?(2d>FVjC%_^ig(-;iI8Lqn@6J?*Nah%EUrPl&^Ddq@wO}W87eX*f%qLfKV5FT4X28AI&dch^7dJf$Qag%@1P|=1X zBn)t)L77svrubrR4GGfCv@^ad0tT{XoZ03}=8-&2p?8GY`sXxJwjoHO&TvQRbqX1P z66sSHQm8A6NN|NOy)QrKZOAy0`P*M&KOc{7jl3r$jZ?M%gSx+;Zp~f2bWp!0mA0?! z#`s6%3$_B_&alVZ$P|A~ky% zCn8fl@LAMKidOE~7i=V2Tk1<;R_78qEO-bE%+z#SNUgz*LB-sm2gcpEP(Nu`%K1l+ z%iZg79vtSQz){gWOoEuVJ<;n~k9R|H4bk5;O=BcloaZ-$3}y+W4SjU|db85l3mtA3 z#v!zEQPU>CbTlCyaZX?gW3Bp?bb%o^Cqravz&m}UZ8y^w0~aTg-H|w+nyX8FNy3yR z#Z9?ra4p@FkA=-JZkr@Ya-7RLpz5S5pPi1tXa9B`Vg|%8QR*U zoUlE&XS>?Zg4Cm(nZ7%A?@Wye2+%U(?zKJ3xf4k*6`kAEOD9gfp1gpfDyZkPXJHi8 z#7_Hdk!8(tGc%=_WG7Smk|;)f7e=6uusj=~o>$G&9IDrNa6``IJW#K@$zzlQCMO|l z{pnGeEQF_0#c1Kb=1ySsvV@%>>=GYwy~GwAD3kz+&u$Nw)N!T7*!&|k71Z#9!Km|G z;E)dlQg715x{fPN6$(GwNOHB{7cD0AT+Jguj}((^ITc{*gH67)wC@<=4Eh#D5d&tw}S^!KBOR43Vo*1XPp`GF^0VUX(1@d>g^G zFkmJ_TS>Y6?kGhHvM&FtxO$7-D2S7iJ_8({4=A*VM@C5Je;V)g)%>&?4WYBZr;>0! z&9grU+=D`A9mL!`E2is@WQP(^qc-Q9se*R}yfw*X;T9Y5i~RI8^dlR0Gg`#=A#KhN zF}=kfXhY!yT`7vTmm8XtVc)8zW54zIM#ACrHmI$Xy04_PMI~i^$7&o&1ImtZXOClq zCWo9r8@a26Q(_xVbrgxY zRV4w}rW1irxp>{JVZ9xM>7d)G+d<4od7 znTs=+1DnHGCBbYW#vcMeH-KcW*@kkf5UTTzAK!n<=$<{`O97Kjd4hCSWk6ZA)3o7v z%RX%~q83~dd7F#_?wIO4$V>!&u-84=pxrv9hh$zhhUrX62`v|$XuiM#+-gg%F~~j= zQn9)8GzT%G3K%Okmqa706x9Q7@8SkU?1lW)1Fzqhj=sLTN+z7zEbfh|KKXcN=2 z5!SI`OAGns9<*wGYsILs4d#oZj%N-Oc+L$h1~E{*Tk`qaCY!E#3(Pg;{`P{rLZ z+Ib-n=(qJ=Ber8M+f5_0`vCWFuBL2m`6jhOa2RK-NFw~-YMLZ&)-nlr^V!m@Vrv9m zklfwwvPZT9vwTIOy55K(UE_}3nvUJh^QR=^`#|5ec%KXW?aJC^l+XzU2>{F@{-0R^ z%dg5@=B!lA3NZ#?)2r|qrhl!+3u^$Eg0+FWsqD~+@|H^+Y#UrtT=m5(C(BfPH19yN zu}9py-TduE$R{-ul&?~=SZsJfiGn~`iWQJh*9Tw#9L1N5#;wtMg(+-@OT)9XzDI-O zukQfaxl^tF?Cb8VgP9hB&KxX+^_)uhrQ$$8 zVEHD_ME%0{Wh+Gl=|hdl1R*cpm9koAPoKYep?~^m;)@k>j*uN*jQ+)24DoL^-stoX z&Oy8VF(O`^Ah{XE>|(>W#&p&WorzC6(*0jNKQRI`OW*kOun;e1$5BtdX!m@^!LXT> zW`7Hc=akL=Fw`5|{`@K#`RCriMQ!X=w}bkKndl|;HSA4^Z~Ao63DP4vYp~tH(0smZKWxCC!3jP6A@MHvIPaUmvq3d3^~MMuY^C;t)RI$R0XDZk z-x0csaG#5omj;ujQd7Lu$Dm_J;8p!5Ro$Yxmp5qR$;qYKfySwQWM6DRi1c;4h2lx) z_RnozEsdnC{1u2!dU717HY^1K)~K>fv8pRkkIGJdtdTiK=*};ahL^gdptx&c(i68b zlD0w%HLoa$P0rB=-`-zY+E3ZTHpMnCC`w-ZC4>0SqcZNK(-isFkDOoegZ8SXFtImQ zaI$xBW;C{UGX1q?yb6#0C!%rI3$xl=(9<*ZWapq?Yt|nfDHgjE>P_>droXfV8`=yYV>HFNTQJ&Le`>yvP~HrV zK+yw4!)Y2oMa!IUo_sW9MW05a83?uE2hBqibSwCQ|DY>E8|c!55nqac=wNXyn<2#8 zIKXiqO*TT6xLHsBRAakRl6A%G?HjJY5R=hqrEzGd4z7?a*%JtMVwJFFH4!l3{&~am zC%CC1g-5u;+{yqZI@mmh4$%bjk4)*&-!KGHrLHtAcwd5|? z3CC4ACdvz%M9^M5Yd+r32eX>1S=`!}sM8+mHAiA2@D9rhgM_r#^)J3xNtlwO^P}%! zFnJ(qdG78K7R8B(J4 z*PS@gn(+?HnQ&{T9p`6X(!c7N^Lwx7h=3<=X~TZC5IaadpWfe}=1}3KwI%ShiFinW z2HxsWqVFMJGUIWNDgC@tJg%>;mdlBas)hCM;^>DcA=pq$3st(TKNL!cflA+?>81es z(gY%^)#!5Ty1%kDSQMwBBpDBTWe)#!Tt8Ve&8Nq1alF zg?e1FQ7!m@hJr|T8d?W2-sUXCSyS1;=Vk{{@(GUXF$NpAR!Z`NP=yb6B2RIT$`jKG zW=`+AJw}iANeZaOS~ZZHZ~l@&S%kM>_|pXCky;DHoPBysKs)2)wmeCdJ@zgr4a?x` zn@69g-9Ir622TH)+Wx)P`d|OyzxuyXUMtA_yMuq%gZ|6#SD*Dtl)tD$e>ePhjoe=i zdtZyk|F>f9cR#-?rvCKw_$nv=O+WR!@$V{(KaF=^_o=VOe`+#*H~syT`A<_p)W1D* z{@uavb@D$QbYcJhf&WL<{C6+ES1kVY;`R0)Pw;#F;&%_f@67-7Fi-Zk10f7Gq*}t3r`|A2v^JLn;nE!LFRgi&t#mFxKG6G=mbyr|T|LfKN15v&D AqyPW_ literal 0 HcmV?d00001