From 1b3d4f727ab58a2b75ab3d68ea602ce2861e9ce9 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:14:17 -0400 Subject: [PATCH 1/7] Single file improvements and debugs (#21) * saving state * updating progress * debugging * debugging --- .../_neuropal_segmentation_interface.py | 4 +- .../interfaces/_optogenetic_stimulation.py | 45 ++-- .../_pump_probe_imaging_interface.py | 29 +-- .../_pump_probe_segmentation_extractor.py | 52 ----- .../_pump_probe_segmentation_interface.py | 196 ++++++++++++------ .../randi_nature_2023/test_all_interfaces.py | 38 ++-- 6 files changed, 180 insertions(+), 184 deletions(-) delete mode 100644 src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_extractor.py diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_segmentation_interface.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_segmentation_interface.py index 2bd89e4..14c49c0 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_segmentation_interface.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_segmentation_interface.py @@ -58,7 +58,7 @@ def add_to_nwbfile( ) nwbfile.add_lab_meta_data(lab_meta_data=imaging_space) else: - imaging_space = nwbfile.lab_meta_data["PlanarImagingSpace"] + imaging_space = nwbfile.lab_meta_data["NeuroPALImagingSpace"] plane_segmentation = ndx_microscopy.MicroscopyPlaneSegmentation( name="NeuroPALPlaneSegmentation", @@ -81,7 +81,7 @@ def add_to_nwbfile( number_of_rois = self.brains_info["nInVolume"][0] for neuropal_roi_id in range(number_of_rois): coordinate_info = self.brains_info["coordZYX"][neuropal_roi_id] - coordinates = (coordinate_info[1], coordinate_info[2], coordinate_info[0], 1.0) + coordinates = (coordinate_info[2], coordinate_info[1], coordinate_info[0], 1.0) plane_segmentation.add_row( id=neuropal_roi_id, diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py index 11f6105..c60df1b 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py @@ -1,6 +1,7 @@ import pathlib from typing import Union +import ndx_microscopy import ndx_patterned_ogen import neuroconv import numpy @@ -36,22 +37,11 @@ def add_to_nwbfile( nwbfile: pynwb.NWBFile, metadata: Union[dict, None] = None, ) -> None: - assert "Microscope" in nwbfile.devices, ( - "The `Microscope` must be added before this interface! Make sure the call to " - "`.run_conversion` for this interface occurs after the `PumpProbeSegmentationInterface`." - ) - microscope = nwbfile.devices["Microscope"] - - assert ( - "ImageSegmentation" in nwbfile.processing["ophys"].data_interfaces - and "PlaneSegmentation" in nwbfile.processing["ophys"]["ImageSegmentation"].plane_segmentations - ), ( - "The `PlaneSegmentation` must be added before this interface! Make sure the call to " - "`.run_conversion` for this interface occurs after the `PumpProbeSegmentationInterface`." - ) - image_segmentation = nwbfile.processing["ophys"]["ImageSegmentation"] - plane_segmentation = image_segmentation.plane_segmentations["PlaneSegmentation"] - imaging_plane = plane_segmentation.imaging_plane + if "Microscope" not in nwbfile.devices: + microscope = ndx_microscopy.Microscope(name="Microscope") + nwbfile.add_device(devices=microscope) + else: + microscope = nwbfile.devices["Microscope"] light_source = ndx_patterned_ogen.LightSource( name="AmplifiedLaser", @@ -101,22 +91,41 @@ def add_to_nwbfile( # Assume all targets are unique; if retargeting of the same location is ever enabled, it would be nice # to refactor this to make proper reuse of target locations. + optical_channel = pynwb.ophys.OpticalChannel( # TODO: I really wish I didn't need this... + name="DummyOpticalChannel", + description="A dummy optical channel for ndx-patterned-ogen metadata.", + emission_lambda=numpy.nan, + ) + imaging_plane = nwbfile.create_imaging_plane( + name="TargetImagingPlane", + description="The targeted plane.", + indicator="", + location="whole brain", + excitation_lambda=numpy.nan, + device=microscope, + optical_channel=optical_channel, + ) targeted_plane_segmentation = pynwb.ophys.PlaneSegmentation( name="TargetPlaneSegmentation", description="Table for storing the target centroids, defined by a one-voxel mask.", imaging_plane=imaging_plane, ) targeted_plane_segmentation.add_column(name="depth_in_um", description="Targeted depth in micrometers.") - for target_x_index, target_y_index, depth_in_mm in zip( + for target_x_index, target_y_index, depth_in_um in zip( self.optogenetic_stimulus_table["optogTargetX"], self.optogenetic_stimulus_table["optogTargetY"], self.optogenetic_stimulus_table["optogTargetZ"], ): targeted_plane_segmentation.add_roi( - pixel_mask=[(int(target_x_index), int(target_y_index), 1.0)], depth_in_um=depth_in_mm * 1e3 + pixel_mask=[(int(target_x_index), int(target_y_index), 1.0)], depth_in_um=depth_in_um ) + + image_segmentation = pynwb.ophys.ImageSegmentation(name="TargetedImageSegmentation") image_segmentation.add_plane_segmentation(targeted_plane_segmentation) + ophys_module = neuroconv.tools.nwb_helpers.get_module(nwbfile=nwbfile, name="ophys") + ophys_module.add(image_segmentation) + # Hardcoded duration from the methods section of paper # TODO: may have to adjust this for unc-31 mutant strain subjects stimulus_duration_in_s = 500.0 / 1e3 diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py index 1b819ca..8c6d299 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py @@ -43,12 +43,16 @@ def __init__( GreenChannel=(slice(0, 512), slice(0, 512)) RedChannel=(slice(512, 1024), slice(0, 512)) """ + super().__init__( + pumpprobe_folder_path=pumpprobe_folder_path, + channel_name=channel_name, + channel_frame_slicing=channel_frame_slicing, + ) if channel_name not in _DEFAULT_CHANNEL_NAMES and channel_frame_slicing is None: - message = ( + raise ValueError( f"A custom `optical_channel_name` was specified ('{channel_name}') and was not one of the " f"known defaults ('{_DEFAULT_CHANNEL_NAMES}'), but no frame slicing pattern was passed." ) - raise ValueError(message=message) self.channel_name = channel_name self.channel_frame_slicing = channel_frame_slicing or _DEFAULT_CHANNEL_FRAME_SLICING[channel_name] @@ -98,21 +102,6 @@ def __init__( full_slice[2].stop - full_slice[2].start, ) - # def get_metadata(self) -> dict: - # one_photon_metadata = super().get_metadata(photon_series_type=self.photon_series_type) - # - # # Hardcoded value from lab - # # This is also an average in a sense - the exact depth is tracked by the Piezo and written - # # as a custom DynamicTable in the ExtraOphysMetadataInterface - # # depth_per_pixel = 0.42 - # - # # one_photon_metadata["Ophys"]["grid_spacing"] = (um_per_pixel, um_per_pixel, um_per_pixel) - # - # return one_photon_metadata - # - # def get_metadata_schema(self) -> dict: - # return super().get_metadata(photon_series_type=self.photon_series_type) - def add_to_nwbfile( self, *, @@ -134,13 +123,13 @@ def add_to_nwbfile( else: light_source = nwbfile.devices["MicroscopyLightSource"] - if "PlanarImagingSpace" not in nwbfile.lab_meta_data: + if "PumpProbeImagingSpace" not in nwbfile.lab_meta_data: imaging_space = ndx_microscopy.PlanarImagingSpace( - name="PlanarImagingSpace", description="", microscope=microscope + name="PumpProbeImagingSpace", description="", microscope=microscope ) nwbfile.add_lab_meta_data(lab_meta_data=imaging_space) else: - imaging_space = nwbfile.lab_meta_data["PlanarImagingSpace"] + imaging_space = nwbfile.lab_meta_data["PumpProbeImagingSpace"] optical_channel = ndx_microscopy.MicroscopyOpticalChannel(name=self.channel_name, description="", indicator="") nwbfile.add_lab_meta_data(lab_meta_data=optical_channel) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_extractor.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_extractor.py deleted file mode 100644 index 20f01f8..0000000 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_extractor.py +++ /dev/null @@ -1,52 +0,0 @@ -import pathlib -import pickle -from typing import Iterable, Literal, Tuple, Union - -import numpy -from roiextractors import SegmentationExtractor - - -class PumpProbeSegmentationExtractor(SegmentationExtractor): - """A very custom segmentation extractor for the .pickle files that correspond to wormdatamodel objects.""" - - def __init__( - self, - *, - file_path: str | pathlib.Path, - timestamps: Iterable[float], - image_shape: Tuple[int, int], - ) -> None: - super().__init__() - self._file_path = file_path - self._times = timestamps - self._image_shape = image_shape - - self._channel_names = [self._file_path.stem.upper()] - - with open(file=file_path, mode="rb") as file: - self._all_pickled_info = pickle.load(file=file) - - self._roi_response_raw = self._all_pickled_info.data - - # TODO: this isn't quite true, I don't think it's normalized - self._roi_response_dff = self._all_pickled_info.derivative - - # TODO - number_of_rois = self._roi_response_raw.shape[1] - self._image_masks = numpy.zeros(shape=(self._image_shape[0], self._image_shape[1], number_of_rois), dtype=bool) - - def get_accepted_list(self) -> list: - return list(range(self._roi_response_raw.shape[1])) - - def get_rejected_list(self) -> list: - return list() - - def get_image_size(self) -> Tuple[int, int]: - return self._image_shape - - # TODO: might want to load these from brains file - def get_roi_ids(self) -> list: - return super().get_roi_ids() - - def get_channel_names(self) -> list: - return self._channel_names diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_interface.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_interface.py index 4acebd6..90da0d4 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_interface.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_segmentation_interface.py @@ -1,17 +1,16 @@ +import json import pathlib +import pickle from typing import Literal +import ndx_microscopy +import neuroconv import numpy import pandas -from neuroconv.datainterfaces.ophys.basesegmentationextractorinterface import ( - BaseSegmentationExtractorInterface, -) -from pynwb import NWBFile +import pynwb -class PumpProbeSegmentationInterface(BaseSegmentationExtractorInterface): - ExtractorModuleName = "leifer_lab_to_nwb.randi_nature_2023.interfaces._pump_probe_segmentation_extractor" - ExtractorName = "PumpProbeSegmentationExtractor" +class PumpProbeSegmentationInterface(neuroconv.basedatainterface.BaseDataInterface): def __init__(self, *, pumpprobe_folder_path: str | pathlib.Path, channel_name: Literal["Green", "Red"] | str): """ @@ -22,73 +21,134 @@ def __init__(self, *, pumpprobe_folder_path: str | pathlib.Path, channel_name: L pumpprobe_folder_path : DirectoryPath Path to the pumpprobe folder. """ + super().__init__(pumpprobe_folder_path=pumpprobe_folder_path, channel_name=channel_name) pumpprobe_folder_path = pathlib.Path(pumpprobe_folder_path) + self.channel_name = channel_name + # Other interfaces use CamelCase to refer to the NWB object the channel data will end up as # The files on the other hand are all lower case lower_channel_name = channel_name.lower() - pickle_file_path = pumpprobe_folder_path / f"{lower_channel_name}.pickle" + signal_file_path = pumpprobe_folder_path / f"{lower_channel_name}.pickle" + with open(file=signal_file_path, mode="rb") as io: + self.signal_info = pickle.load(file=io) + + mask_type_info = {key: self.signal_info.info[key] for key in ["method", "ref_index", "version"]} + expected_mask_type_info = {"method": "box", "ref_index": 30, "version": "1.5"} + assert mask_type_info == expected_mask_type_info, ( + "Unimplemented method detected for mask type." + f"\n\nReceived: {mask_type_info}" + f"\n\nExpected: {expected_mask_type_info}" + "\n\nPlease raise an issue to have the new mask type incorporated." + ) - # TODO: generalize this timestamp extraction to a common utility function - # From prototyping data, the frameSync seems to start first... - sync_table_file_path = pumpprobe_folder_path / "other-frameSynchronous.txt" - sync_table = pandas.read_table(filepath_or_buffer=sync_table_file_path, index_col=False) - frame_indices = sync_table["Frame index"] + brains_file_path = pumpprobe_folder_path / "brains.json" + with open(brains_file_path, "r") as io: + self.brains_info = json.load(fp=io) - # ...then the frameDetails has timestamps for a subset of the frame indices + # Technically every frame at every depth has a timestamp (and these are in the source MicroscopySeries) + # But the fluorescence is aggregated per volume (over time) and so the timestamps are averaged over those frames timestamps_file_path = pumpprobe_folder_path / "framesDetails.txt" timestamps_table = pandas.read_table(filepath_or_buffer=timestamps_file_path, index_col=False) - number_of_frames = timestamps_table.shape[0] - - frame_count_delay = timestamps_table["frameCount"][0] - frame_indices[0] - frame_count_end = frame_count_delay + number_of_frames - - sync_subtable = sync_table.iloc[frame_count_delay:frame_count_end] - - self.timestamps = numpy.array(timestamps_table["Timestamp"]) - - # Hardcoding this for now - image_shape = (512, 512) - super().__init__(file_path=pickle_file_path, timestamps=self.timestamps, image_shape=image_shape) - - # Hardcode a special plane segmentation name for this interface - # self.plane_segmentation_name = "PumpProbeSegmentation" - - # def get_metadata(self) -> dict: - # metadata = super().get_metadata() - # - # # Hardcoded value from lab - # # This is also an average in a sense - the exact depth is tracked by the Piezo and written - # # as a custom DynamicTable in the ExtraOphysMetadataInterface - # depth_per_pixel = 0.42 - # - # # one_photon_metadata["Ophys"]["grid_spacing"] = (um_per_pixel, um_per_pixel, um_per_pixel) - # - # metadata["Ophys"]["ImageSegmentation"]["plane_segmentations"][0]["name"] = self.plane_segmentation_name - # - # metadata["Ophys"]["Fluorescence"]= {'name': 'Fluorescence', self.plane_segmentation_name: {'raw': { - # 'name': 'BaselineSignal', 'description': 'Array of raw fluorescence traces.', 'unit': 'n.a.'}}} - # metadata["Ophys"]["DfOverF"] = {'name': 'Derivative', self.plane_segmentation_name: { - # 'dff': {'name': 'DerivativeOfSignal', 'description': 'Array of filtered fluorescence traces; ' - # 'approximately the derivative (unnormalized) of the ' - # 'baseline signal' - # '.', 'unit': 'n.a.'}}} - # - # return metadata - # - # def get_metadata_schema(self) -> dict: - # return super().get_metadata(photon_series_type="OnePhotonSeries") - - # def add_to_nwbfile( - # self, - # *, - # nwbfile: NWBFile, - # metadata: dict | None = None, - # stub_test: bool = False, - # ) -> None: - # super().add_to_nwbfile( - # nwbfile=nwbfile, - # metadata=metadata, - # stub_test=stub_test, - # plane_segmentation_name="PumpProbeSegmentation", - # ) + timestamps = numpy.array(timestamps_table["Timestamp"]) + + averaged_timestamps = numpy.empty(shape=self.signal_info.data.shape[0], dtype=numpy.float64) + z_of_frame_lengths = [len(entry) for entry in self.brains_info["zOfFrame"]] + cumulative_sum_of_lengths = numpy.cumsum(z_of_frame_lengths) + for volume_index, (z_of_frame_length, cumulative_sum) in enumerate( + zip(z_of_frame_lengths, cumulative_sum_of_lengths) + ): + start_frame = cumulative_sum - z_of_frame_length + end_frame = cumulative_sum + averaged_timestamps[volume_index] = numpy.mean(timestamps[start_frame:end_frame]) + + self.timestamps_per_volume = averaged_timestamps + + def add_to_nwbfile( + self, + *, + nwbfile: pynwb.NWBFile, + metadata: dict | None = None, + stub_test: bool = False, + stub_frames: int = 70, + ) -> None: + # TODO: probably centralize this in a helper function + if "Microscope" not in nwbfile.devices: + microscope = ndx_microscopy.Microscope(name="Microscope") + nwbfile.add_device(devices=microscope) + else: + microscope = nwbfile.devices["Microscope"] + + if "PumpProbeImagingSpace" not in nwbfile.lab_meta_data: + imaging_space = ndx_microscopy.PlanarImagingSpace( + name="PumpProbeImagingSpace", description="", microscope=microscope + ) + nwbfile.add_lab_meta_data(lab_meta_data=imaging_space) + else: + imaging_space = nwbfile.lab_meta_data["PumpProbeImagingSpace"] + + plane_segmentation = ndx_microscopy.MicroscopyPlaneSegmentation( + name=f"PumpProbe{self.channel_name}PlaneSegmentation", + description=( + "The PumpProbe segmentation of the C. elegans brain. " + "Only some of these local ROI IDs match the NeuroPAL IDs with cell labels. " + "Note that the Z-axis of the `voxel_mask` is in reference to the index of that depth in its scan cycle." + ), + imaging_space=imaging_space, + ) + plane_segmentation.add_column( + name="neuropal_ids", + description=( + "The NeuroPAL ROI ID that has been matched to this PumpProbe ID. Blank means the ROI was not matched." + ), + ) + + # There are coords for each 'nInVolume', but only the ones for the span of the 30th frame are used + number_of_rois = self.signal_info.data.shape[1] + labeled_frame_index = 30 + sub_start = sum(self.brains_info["nInVolume"][:labeled_frame_index]) + sub_coordinates = self.brains_info["coordZYX"][sub_start : (sub_start + number_of_rois)] + + number_of_rois = self.brains_info["nInVolume"][labeled_frame_index] + for pump_probe_roi_id in range(number_of_rois): + coordinate_info = sub_coordinates[pump_probe_roi_id] + coordinates = (coordinate_info[2], coordinate_info[1], coordinate_info[0], 1.0) + + plane_segmentation.add_row( + id=pump_probe_roi_id, + voxel_mask=[coordinates], # TODO: add rest of box + neuropal_ids=self.brains_info["labels"][labeled_frame_index][pump_probe_roi_id].replace(" ", ""), + ) + + # TODO: might prefer to combine plane segmentations over image segmentation objects + # to reduce clutter + image_segmentation = ndx_microscopy.MicroscopyImageSegmentation( + name=f"PumpProbe{self.channel_name}ImageSegmentation", microscopy_plane_segmentations=[plane_segmentation] + ) + + ophys_module = neuroconv.tools.nwb_helpers.get_module(nwbfile=nwbfile, name="ophys") + ophys_module.add(image_segmentation) + + plane_segmentation_region = pynwb.ophys.DynamicTableRegion( + name="table_region", # Name must be exactly this + description="", + data=[x for x in range(number_of_rois)], + table=plane_segmentation, + ) + microscopy_response_series = ndx_microscopy.MicroscopyResponseSeries( + name=f"{self.channel_name}Signal", + description=( + f"Average baseline fluorescence for the '{self.channel_name}' optical channel extracted from the raw " + "imaging and averaged over a volume defined as a complete scan cycle over volumetric depths." + ), + data=self.signal_info.data, + table_region=plane_segmentation_region, + unit="n.a.", + timestamps=self.timestamps_per_volume, + ) + + # TODO: should probably combine all of these into a single container + container = ndx_microscopy.MicroscopyResponseSeriesContainer( + name=f"{self.channel_name}Signals", microscopy_response_series=[microscopy_response_series] + ) + ophys_module.add(container) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py index f101bad..3b18aa8 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py @@ -12,6 +12,7 @@ import pandas import pynwb from dateutil import tz +from pynwb.testing.mock.file import mock_NWBFile from leifer_lab_to_nwb.randi_nature_2023 import RandiNature2023Converter from leifer_lab_to_nwb.randi_nature_2023.interfaces import ( @@ -45,42 +46,38 @@ session_start_time = datetime.datetime.strptime(session_string, "%Y%m%d_%H%M%S") session_start_time = session_start_time.replace(tzinfo=tz.gettz("US/Eastern")) +# TODO: might be able to remove these when NeuroConv supports better schema validation +PUMPPROBE_FOLDER_PATH = str(PUMPPROBE_FOLDER_PATH) +MULTICOLOR_FOLDER_PATH = str(MULTICOLOR_FOLDER_PATH) + interfaces_classes_to_test = { "PumpProbeImagingInterfaceGreen": { - "class": PumpProbeImagingInterface, "source_data": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH, "channel_name": "Green"}, "conversion_options": {"stub_test": True}, }, "PumpProbeImagingInterfaceRed": { - "class": PumpProbeImagingInterface, "source_data": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH, "channel_name": "Red"}, "conversion_options": {"stub_test": True}, }, "PumpProbeSegmentationInterfaceGreed": { - "class": PumpProbeSegmentationInterface, "source_data": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH, "channel_name": "Green"}, "conversion_options": {"stub_test": True}, }, "PumpProbeSegmentationInterfaceRed": { - "class": PumpProbeSegmentationInterface, "source_data": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH, "channel_name": "Red"}, "conversion_options": {"stub_test": True}, }, "NeuroPALImagingInterface": { - "class": NeuroPALImagingInterface, "source_data": {"multicolor_folder_path": MULTICOLOR_FOLDER_PATH}, "conversion_options": {"stub_test": True}, }, "NeuroPALSegmentationInterface": { - "class": NeuroPALSegmentationInterface, "source_data": {"multicolor_folder_path": MULTICOLOR_FOLDER_PATH}, }, "OptogeneticStimulationInterface": { - "class": OptogeneticStimulationInterface, "source_data": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH}, }, "ExtraOphysMetadataInterface": { - "class": ExtraOphysMetadataInterface, "source_data": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH}, }, } @@ -89,21 +86,8 @@ for test_case_name, interface_options in interfaces_classes_to_test.items(): nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"test_{test_case_name}.nwb" - data_interfaces = list() - - # Special case of the OptogeneticStimulationInterface; requires the PumpProbeSegmentationInterface to be added first - if test_case_name == "OptogeneticStimulationInterface": - pump_probe_segmentation_interface = PumpProbeSegmentationInterface( - pumpprobe_folder_path=PUMPPROBE_FOLDER_PATH, channel_name="Green" - ) - data_interfaces.append(pump_probe_segmentation_interface) - - InterfaceClassToTest = interface_options["class"] - interface = InterfaceClassToTest(**interface_options["source_data"]) - data_interfaces.append(interface) - - # Initialize converter - converter = RandiNature2023Converter(data_interfaces=data_interfaces) + source_data = {test_case_name: interface_options["source_data"]} + converter = RandiNature2023Converter(source_data=source_data) metadata = converter.get_metadata() @@ -120,9 +104,15 @@ metadata["Subject"]["cultivation_temp"] = 20.0 if "conversion_options" in interface_options: - conversion_options = {InterfaceClassToTest.__name__: interface_options["conversion_options"]} + conversion_options = {test_case_name: interface_options["conversion_options"]} else: conversion_options = None + + in_memory_nwbfile = mock_NWBFile() + converter.add_to_nwbfile(nwbfile=in_memory_nwbfile, metadata=metadata, conversion_options=conversion_options) + + print("Added to in-memory NWBFile object!") + converter.run_conversion( nwbfile_path=nwbfile_path, metadata=metadata, conversion_options=conversion_options, overwrite=True ) From 5c0ef97ef3b85ff77d52c43ec7f119e6b1b5c6ec Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Thu, 27 Jun 2024 15:15:55 -0400 Subject: [PATCH 2/7] fix point spread estimate --- .../randi_nature_2023/interfaces/_optogenetic_stimulation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py index c60df1b..f7e5a8e 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py @@ -84,8 +84,9 @@ def add_to_nwbfile( ), # Calculated manually from the 'source data' of Supplementary Figure 2a # https://www.nature.com/articles/s41586-023-06683-4#MOESM10 - lateral_point_spread_function_in_um="(-0.245, 0.059) ± (0.396, 0.264)", - axial_point_spread_function_in_um="0.444 ± 0.536", + # via https://github.com/catalystneuro/leifer_lab_to_nwb/issues/5#issuecomment-2195497434 + lateral_point_spread_function_in_um="(-0.246, 2.21) ± (0.045, 1.727)", + axial_point_spread_function_in_um="0.540 ± 1.984", ) nwbfile.add_lab_meta_data(temporal_focusing) From e6a8a4711e2818d5adc6d9ce01cbf5521034727e Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 28 Jun 2024 09:39:25 -0400 Subject: [PATCH 3/7] distinguish path --- .../randi_nature_2023/convert_session.py | 10 +++++----- .../randi_nature_2023/test_all_interfaces.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py index 3621392..ee08123 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py @@ -72,11 +72,11 @@ metadata["Subject"]["cultivation_temp"] = 20.0 conversion_options = { - "PumpProbeImagingInterfaceGreen": {"stub_test": True}, - "PumpProbeImagingInterfaceRed": {"stub_test": True}, - "PumpProbeSegmentationInterfaceGreed": {"stub_test": True}, - "PumpProbeSegmentationInterfaceRed": {"stub_test": True}, - "NeuroPALImagingInterface": {"stub_test": True}, + "PumpProbeImagingInterfaceGreen": {"stub_test": STUB_TEST}, + "PumpProbeImagingInterfaceRed": {"stub_test": STUB_TEST}, + "PumpProbeSegmentationInterfaceGreed": {"stub_test": STUB_TEST}, + "PumpProbeSegmentationInterfaceRed": {"stub_test": STUB_TEST}, + "NeuroPALImagingInterface": {"stub_test": STUB_TEST}, } nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"sub-{subject_id}_ses-{session_string}.nwb" diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py index 3b18aa8..8b3415a 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py @@ -33,7 +33,7 @@ PUMPPROBE_FOLDER_PATH = SESSION_FOLDER_PATH / "pumpprobe_20211104_163944" MULTICOLOR_FOLDER_PATH = SESSION_FOLDER_PATH / "multicolorworm_20211104_162630" -NWB_OUTPUT_FOLDER_PATH = BASE_FOLDER_PATH / "nwbfiles" +NWB_OUTPUT_FOLDER_PATH = BASE_FOLDER_PATH / "test_nwbfiles" # ************************************************************************* # Everything below this line is automated and should not need to be changed From 3d8c80b635d47627c10c2cbc8fd7c6688230c60f Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 28 Jun 2024 10:07:05 -0400 Subject: [PATCH 4/7] including stub in name --- src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py index ee08123..adf9b3f 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py @@ -79,7 +79,8 @@ "NeuroPALImagingInterface": {"stub_test": STUB_TEST}, } -nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"sub-{subject_id}_ses-{session_string}.nwb" +file_stem = f"sub-{subject_id}_ses-{session_string}" if not STUB_TEST else f"{session_string}_stub" +nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"{file_stem}.nwb" converter.run_conversion( nwbfile_path=nwbfile_path, metadata=metadata, overwrite=True, conversion_options=conversion_options ) From 288c01fd814e800ba160148c4af98614528e4a00 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 28 Jun 2024 10:13:39 -0400 Subject: [PATCH 5/7] simplify --- .../randi_nature_2023/convert_session.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py index adf9b3f..54ad208 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py @@ -40,9 +40,6 @@ PUMPPROBE_FOLDER_PATH = str(PUMPPROBE_FOLDER_PATH) MULTICOLOR_FOLDER_PATH = str(MULTICOLOR_FOLDER_PATH) -# Initialize interfaces -data_interfaces = list() - source_data = { "PumpProbeImagingInterfaceGreen": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH, "channel_name": "Green"}, "PumpProbeImagingInterfaceRed": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH, "channel_name": "Red"}, @@ -54,7 +51,6 @@ "ExtraOphysMetadataInterface": {"pumpprobe_folder_path": PUMPPROBE_FOLDER_PATH}, } -# Initialize converter converter = RandiNature2023Converter(source_data=source_data) metadata = converter.get_metadata() @@ -79,8 +75,14 @@ "NeuroPALImagingInterface": {"stub_test": STUB_TEST}, } -file_stem = f"sub-{subject_id}_ses-{session_string}" if not STUB_TEST else f"{session_string}_stub" -nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"{file_stem}.nwb" +if STUB_TEST: + nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"{session_string}_stub.nwb" +else: + # Name and nest the file in a DANDI compliant way + subject_folder_path = NWB_OUTPUT_FOLDER_PATH / f"sub-{subject_id}" + subject_folder_path.mkdir(exist_ok=True) + nwbfile_path = subject_folder_path / f"sub-{subject_id}_ses-{session_string}.nwb" + converter.run_conversion( nwbfile_path=nwbfile_path, metadata=metadata, overwrite=True, conversion_options=conversion_options ) From 25ed4d0630152d7bba1125b5ba7749f79eb03281 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 28 Jun 2024 10:37:43 -0400 Subject: [PATCH 6/7] debugs for warning and invaldiation --- .../randi_nature_2023/convert_session.py | 4 ++++ .../interfaces/_optogenetic_stimulation.py | 21 ++++++++++++------- .../randi_nature_2023/test_all_interfaces.py | 4 ++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py index 54ad208..3db3962 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py @@ -2,6 +2,7 @@ import datetime import pathlib +import warnings import pandas import pynwb @@ -29,6 +30,9 @@ # Everything below this line is automated and should not need to be changed # ************************************************************************* +# Suppress false warning +warnings.filterwarnings(action="ignore", message="The linked table for DynamicTableRegion*", category=UserWarning) + NWB_OUTPUT_FOLDER_PATH.mkdir(exist_ok=True) # Parse session start time from the pumpprobe path diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py index f7e5a8e..8b5fc51 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_optogenetic_stimulation.py @@ -37,11 +37,16 @@ def add_to_nwbfile( nwbfile: pynwb.NWBFile, metadata: Union[dict, None] = None, ) -> None: - if "Microscope" not in nwbfile.devices: - microscope = ndx_microscopy.Microscope(name="Microscope") - nwbfile.add_device(devices=microscope) - else: - microscope = nwbfile.devices["Microscope"] + # if "Microscope" not in nwbfile.devices: + # microscope = ndx_microscopy.Microscope(name="Microscope") + # nwbfile.add_device(devices=microscope) + # else: + # microscope = nwbfile.devices["Microscope"] + + # TODO: reusing the Microscope device creates an invalid file + # NWB team has been notified about the issue, until then, we need to create a dummy device + ogen_device = pynwb.ophys.Device(name="OptogeneticDevice", description="") + nwbfile.add_device(ogen_device) light_source = ndx_patterned_ogen.LightSource( name="AmplifiedLaser", @@ -64,7 +69,8 @@ def add_to_nwbfile( excitation_lambda=850.0, # nm effector="GUR-3/PRDX-2", location="whole brain", - device=microscope, + # device=microscope, + device=ogen_device, light_source=light_source, ) nwbfile.add_ogen_site(site) @@ -103,7 +109,8 @@ def add_to_nwbfile( indicator="", location="whole brain", excitation_lambda=numpy.nan, - device=microscope, + # device=microscope, + device=ogen_device, optical_channel=optical_channel, ) targeted_plane_segmentation = pynwb.ophys.PlaneSegmentation( diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py index 8b3415a..852ad0c 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py @@ -8,6 +8,7 @@ import datetime import pathlib +import warnings import pandas import pynwb @@ -39,6 +40,9 @@ # Everything below this line is automated and should not need to be changed # ************************************************************************* +# Suppress false warning +warnings.filterwarnings(action="ignore", message="The linked table for DynamicTableRegion*", category=UserWarning) + NWB_OUTPUT_FOLDER_PATH.mkdir(exist_ok=True) # Parse session start time from the pumpprobe path From c8463751fb74c933dd81d259d763efdc92fa8b74 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 28 Jun 2024 12:25:33 -0400 Subject: [PATCH 7/7] distinct folder paths --- .../randi_nature_2023/convert_session.py | 33 +++++++++++++++---- .../interfaces/_neuropal_imaging_interface.py | 2 ++ .../_pump_probe_imaging_interface.py | 7 +++- .../randi_nature_2023/test_all_interfaces.py | 7 ++-- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py index 3db3962..d307b74 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/convert_session.py @@ -12,7 +12,7 @@ # STUB_TEST=True creates 'preview' files that truncate all major data blocks; useful for ensuring process runs smoothly # STUB_TEST=False performs a full file conversion -STUB_TEST = True +STUB_TEST = False # Define base folder of source data @@ -61,14 +61,32 @@ metadata["NWBFile"]["session_start_time"] = session_start_time -# TODO: these are placeholders that would be read in from a logbook read+lookup +# TODO: these are all placeholders that would be read in from the YAML logbook read+lookup +metadata["NWBFile"][ + "experiment_description" +] = """ +To measure signal propagation, we activated each single neuron, one at a time, through two-photon stimulation, +while simultaneously recording the calcium activity of the population at cellular resolution using spinning disk +confocal microscopy. We recorded activity from 113 wild-type (WT)-background animals, each for up to 40min, while +stimulating a mostly randomly selected sequence of neurons one by one every 30s. We spatially restricted our +two-photon activation in three dimensions to be the size of a typical C. elegans neuron, to minimize off-target +activation of neighbouring neurons. Animals were immobilized but awake,and pharyngeal pumping was visible during +recordings. +""" +metadata["NWBFile"]["institution"] = "Princeton University" +metadata["NWBFile"]["lab"] = "Leifer Lab" +metadata["NWBFile"]["experimenter"] = ["Randi, Francesco"] +metadata["NWBFile"]["keywords"] = ["C. elegans", "optogenetics", "functional connectivity"] + subject_id = session_start_time.strftime("%y%m%d") metadata["Subject"]["subject_id"] = subject_id -metadata["Subject"]["species"] = "C. elegans" +metadata["Subject"]["species"] = "Caenorhabditis elegans" +metadata["Subject"]["strain"] = "AKS471.2.d" +metadata["Subject"]["genotype"] = "WT" metadata["Subject"]["sex"] = "XX" metadata["Subject"]["age"] = "P1D" # metadata["Subject"]["growth_stage_time"] = pandas.Timedelta(hours=2, minutes=30).isoformat() # TODO: request -metadata["Subject"]["growth_stage"] = "YA" +metadata["Subject"]["growth_stage"] = "L4" metadata["Subject"]["cultivation_temp"] = 20.0 conversion_options = { @@ -80,12 +98,15 @@ } if STUB_TEST: - nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"{session_string}_stub.nwb" + stub_folder_path = NWB_OUTPUT_FOLDER_PATH / "stubs" + stub_folder_path.mkdir(exist_ok=True) + nwbfile_path = stub_folder_path / f"{session_string}_stub.nwb" else: # Name and nest the file in a DANDI compliant way subject_folder_path = NWB_OUTPUT_FOLDER_PATH / f"sub-{subject_id}" subject_folder_path.mkdir(exist_ok=True) - nwbfile_path = subject_folder_path / f"sub-{subject_id}_ses-{session_string}.nwb" + dandi_session_string = session_string.replace("_", "-") + nwbfile_path = subject_folder_path / f"sub-{subject_id}_ses-{dandi_session_string}.nwb" converter.run_conversion( nwbfile_path=nwbfile_path, metadata=metadata, overwrite=True, conversion_options=conversion_options diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_imaging_interface.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_imaging_interface.py index d32c22e..268e16e 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_imaging_interface.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_neuropal_imaging_interface.py @@ -115,8 +115,10 @@ def add_to_nwbfile( chunk_shape = (1, 1, self.data_shape[-2], self.data_shape[-1]) # Best we can do is limit the number of depths that are written by stub + # TODO: add ndx-micorscopy support to NeuroConv BackendConfiguration to avoid need for H5DataIO imaging_data = self.data if not stub_test else self.data[:stub_depths, :, :, :] data_iterator = neuroconv.tools.hdmf.SliceableDataChunkIterator(data=imaging_data, chunk_shape=chunk_shape) + data_iterator = pynwb.H5DataIO(data_iterator, compression="gzip") depth_per_frame_in_um = self.brains_info["zOfFrame"][0] diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py index 8c6d299..bd81f26 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/interfaces/_pump_probe_imaging_interface.py @@ -109,6 +109,7 @@ def add_to_nwbfile( metadata: dict | None = None, stub_test: bool = False, stub_frames: int = 70, + display_progress: bool = True, ) -> None: # TODO: enhance all metadata if "Microscope" not in nwbfile.devices: @@ -143,8 +144,12 @@ def add_to_nwbfile( num_frames_per_chunk = int(chunk_size_bytes / frame_size_bytes) chunk_shape = (max(min(num_frames_per_chunk, num_frames), 1), x, y) + # TODO: add ndx-micorscopy support to NeuroConv BackendConfiguration to avoid need for H5DataIO imaging_data = self.imaging_data_for_channel if not stub_test else self.imaging_data_for_channel[:stub_frames] - data_iterator = neuroconv.tools.hdmf.SliceableDataChunkIterator(data=imaging_data, chunk_shape=chunk_shape) + data_iterator = neuroconv.tools.hdmf.SliceableDataChunkIterator( + data=imaging_data, chunk_shape=chunk_shape, display_progress=display_progress + ) + data_iterator = pynwb.H5DataIO(data_iterator, compression="gzip") timestamps = self.timestamps if not stub_test else self.timestamps[:stub_frames] diff --git a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py index 852ad0c..51cf325 100644 --- a/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py +++ b/src/leifer_lab_to_nwb/randi_nature_2023/test_all_interfaces.py @@ -34,7 +34,7 @@ PUMPPROBE_FOLDER_PATH = SESSION_FOLDER_PATH / "pumpprobe_20211104_163944" MULTICOLOR_FOLDER_PATH = SESSION_FOLDER_PATH / "multicolorworm_20211104_162630" -NWB_OUTPUT_FOLDER_PATH = BASE_FOLDER_PATH / "test_nwbfiles" +NWB_OUTPUT_FOLDER_PATH = BASE_FOLDER_PATH / "nwbfiles" # ************************************************************************* # Everything below this line is automated and should not need to be changed @@ -44,6 +44,8 @@ warnings.filterwarnings(action="ignore", message="The linked table for DynamicTableRegion*", category=UserWarning) NWB_OUTPUT_FOLDER_PATH.mkdir(exist_ok=True) +test_folder_path = NWB_OUTPUT_FOLDER_PATH / "test_interfaces" +test_folder_path.mkdir(exist_ok=True) # Parse session start time from the pumpprobe path session_string = PUMPPROBE_FOLDER_PATH.stem.removeprefix("pumpprobe_") @@ -88,8 +90,6 @@ for test_case_name, interface_options in interfaces_classes_to_test.items(): - nwbfile_path = NWB_OUTPUT_FOLDER_PATH / f"test_{test_case_name}.nwb" - source_data = {test_case_name: interface_options["source_data"]} converter = RandiNature2023Converter(source_data=source_data) @@ -117,6 +117,7 @@ print("Added to in-memory NWBFile object!") + nwbfile_path = test_folder_path / f"test_{test_case_name}.nwb" converter.run_conversion( nwbfile_path=nwbfile_path, metadata=metadata, conversion_options=conversion_options, overwrite=True )