diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aebeef6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +## [0.1.10] (Unreleased) + +### Release Notes + +### Optogenetics + +- fix hfpy write error when different number of spatial node regions between epochs #135 +- Run `add_optogenetic_epochs` in the create nwb function #135 diff --git a/src/trodes_to_nwb/convert.py b/src/trodes_to_nwb/convert.py index be73cff..e4c7cee 100644 --- a/src/trodes_to_nwb/convert.py +++ b/src/trodes_to_nwb/convert.py @@ -17,7 +17,7 @@ from trodes_to_nwb.convert_dios import add_dios from trodes_to_nwb.convert_ephys import RecFileDataChunkIterator, add_raw_ephys from trodes_to_nwb.convert_intervals import add_epochs, add_sample_count -from trodes_to_nwb.convert_optogenetics import add_optogenetics +from trodes_to_nwb.convert_optogenetics import add_optogenetic_epochs, add_optogenetics from trodes_to_nwb.convert_position import add_associated_video_files, add_position from trodes_to_nwb.convert_rec_header import ( add_header_device, @@ -109,6 +109,7 @@ def create_nwbs( output_dir: str = "/stelmo/nwb/raw", video_directory: str = "", convert_video: bool = False, + fs_gui_dir: str = "", n_workers: int = 1, query_expression: str | None = None, disable_ptp: bool = False, @@ -131,6 +132,8 @@ def create_nwbs( Directory containing the video files, by default "". convert_video : bool, optional Whether to convert the video files, by default False. + fs_gui_dir : str, optional + Optional alternative directory to find optogenetic files, by default "". n_workers : int, optional Number of workers to use for parallel processing, by default 1. query_expression : str, optional @@ -170,6 +173,8 @@ def pass_func(args): output_dir, video_directory, convert_video, + fs_gui_dir, + disable_ptp, behavior_only=behavior_only, ) return True @@ -198,6 +203,7 @@ def pass_func(args): output_dir, video_directory, convert_video, + fs_gui_dir, disable_ptp, behavior_only=behavior_only, ) @@ -211,6 +217,7 @@ def _create_nwb( output_dir: str = "/stelmo/nwb/raw", video_directory: str = "", convert_video: bool = False, + fs_gui_dir: str = "", disable_ptp: bool = False, behavior_only: bool = False, ): @@ -311,6 +318,7 @@ def _create_nwb( session_df=session_df, neo_io=rec_dci.neo_io, ) + add_optogenetic_epochs(nwb_file, metadata, fs_gui_dir) logger.info("ADDING POSITION") # add position if disable_ptp: diff --git a/src/trodes_to_nwb/convert_optogenetics.py b/src/trodes_to_nwb/convert_optogenetics.py index 732adb6..0123995 100644 --- a/src/trodes_to_nwb/convert_optogenetics.py +++ b/src/trodes_to_nwb/convert_optogenetics.py @@ -46,7 +46,7 @@ def add_optogenetics(nwbfile: NWBFile, metadata: dict, device_metadata: List[dic return # Add optogenetic experiment metadata - + logger.info("Adding optogenetic experiment metadata") virus, virus_injection = make_virus_injecton( metadata.get("virus_injection"), device_metadata ) @@ -306,11 +306,12 @@ def add_optogenetic_epochs( file_dir : str, optional Directory appended to the file path given in the metadata. Default is empty string. """ - + logger = logging.getLogger("convert") opto_epochs_metadata = metadata.get("fs_gui_yamls", []) if len(opto_epochs_metadata) == 0: - print("No optogenetic epochs found in metadata.") + logger.info("No optogenetic epochs found in metadata.") return + logger.info(f"Adding {len(opto_epochs_metadata)} optogenetic epochs.") from ndx_franklab_novela import FrankLabOptogeneticEpochsTable @@ -320,14 +321,47 @@ def add_optogenetic_epochs( ) # loop through each fsgui script, which can apply to multiple epochs + rows = [] for fs_gui_metadata in opto_epochs_metadata: new_rows = compile_opto_entries( fs_gui_metadata=fs_gui_metadata, nwbfile=nwbfile, file_dir=file_dir, ) - for row in new_rows: - opto_epochs_table.add_row(**row) + rows.extend(new_rows) + + # ensure spatial geometry nodes have same shape in each row + max_shape = [0, 0, 0] # n_regions, n_nodes, 2 (x,y) + for row in rows: + nodes = row.get("spatial_filter_region_node_coordinates_in_pixels", None) + if nodes is not None: + max_shape = [ + max(max_shape[0], nodes.shape[0]), + max(max_shape[1], nodes.shape[1]), + max(max_shape[2], nodes.shape[2]), + ] + if max_shape != [0, 0, 0]: + for row in rows: + nodes = row.get("spatial_filter_region_node_coordinates_in_pixels", None) + if nodes is None: + row["spatial_filter_region_node_coordinates_in_pixels"] = ( + np.ones(max_shape) * np.nan + ) + else: + nodes = np.pad( + nodes, + ( + (0, max_shape[0] - nodes.shape[0]), + (0, max_shape[1] - nodes.shape[1]), + (0, max_shape[2] - nodes.shape[2]), + ), + mode="constant", + constant_values=np.nan, + ) + row["spatial_filter_region_node_coordinates_in_pixels"] = nodes + + for row in rows: + opto_epochs_table.add_row(**row) nwbfile.add_time_intervals(opto_epochs_table) diff --git a/src/trodes_to_nwb/tests/test_convert.py b/src/trodes_to_nwb/tests/test_convert.py index c5a1bf6..d9aab0c 100644 --- a/src/trodes_to_nwb/tests/test_convert.py +++ b/src/trodes_to_nwb/tests/test_convert.py @@ -61,6 +61,7 @@ def test_convert_full(): output_dir=str(data_path), n_workers=1, query_expression=f"animal == 'sample' and full_path != '{exclude_reconfig_yaml}'", + fs_gui_dir=data_path, ) output_file_path = data_path / "sample20230622.nwb" @@ -105,6 +106,7 @@ def do_nothing(nwbfile, metadata_dict): path=data_path, device_metadata_paths=device_metadata, output_dir=str(data_path), + fs_gui_dir=data_path, n_workers=1, query_expression=f"animal == 'sample' and full_path != '{exclude_reconfig_yaml}'", ) diff --git a/src/trodes_to_nwb/tests/test_convert_optogenetics.py b/src/trodes_to_nwb/tests/test_convert_optogenetics.py index 3ebe19e..b2843b2 100644 --- a/src/trodes_to_nwb/tests/test_convert_optogenetics.py +++ b/src/trodes_to_nwb/tests/test_convert_optogenetics.py @@ -75,7 +75,7 @@ def test_add_optogenetic_epochs(): np.allclose(opto_df.stop_time.values, np.array([1.68747481e09, 1.68747484e09])) assert opto_df.stimulation_on.values[0] assert opto_df.spatial_filter_region_node_coordinates_in_pixels.values[0].shape == ( - 1, + 3, 7, 2, )