Skip to content

Commit c98968b

Browse files
committed
Merge branch 'main' into save_uv
2 parents 141949d + 9541a17 commit c98968b

39 files changed

+1632
-112
lines changed

.github/workflows/test_package_build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ jobs:
104104
with:
105105
# Path to cache
106106
path: ${{ env.DOWNLOAD_DIR }}
107-
# Cache key: OS + static string + version number (bump v1 to v2 to invalidate)
108-
key: ${{ runner.os }}-testdata-trodes-v1
107+
# Cache key: OS + static string + version number (bump v2 to v3 to invalidate)
108+
key: ${{ runner.os }}-testdata-trodes-v2
109109

110110
# --- Download Test Data Step (Conditional) ---
111111
- name: Download test rec files

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Change Log
2+
3+
## [0.1.10] (Unreleased)
4+
5+
### Release Notes
6+
7+
### Optogenetics
8+
9+
- fix hfpy write error when different number of spatial node regions between epochs #135
10+
- Run `add_optogenetic_epochs` in the create nwb function #135

environment.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ dependencies:
77
- numpy
88
- scipy
99
- pandas
10-
- pynwb
10+
- pynwb<=3.0.0
1111
- nwbinspector>=0.5.0
1212
- dask
1313
- ipython
@@ -20,4 +20,5 @@ dependencies:
2020
- pip
2121
- pip:
2222
- ndx-franklab-novela
23+
- ndx-optogenetics
2324
- neo>=0.13.4

notebooks/conversion_tutorial.ipynb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
"Where the nwb file will be saved. For Frank Lab members, this should be `/stelmo/nwb/raw/`. File is named by the convention {animal}{date}.nwb\n",
4949
"\n",
5050
"+ `query_expression`: A query expression to select which files to convert. For example, if you have several animals in your folder, you could write `\"animal == 'sample'\"` to select only the sample animal. Defaults to `None` which converts all files in the directory (potentially overwriting ones you've done before!).\n",
51+
"\n",
52+
"+ `behavior_only`: Rec files recorded through trodes software without e-phys data have a\n",
53+
" different expected internal structure. Use this flag to ensure correct data parsing \n",
54+
" is used. Default of `False` expects electrophysiology data to be present\n",
5155
"\n"
5256
]
5357
},

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ dependencies = [
2424
"numpy",
2525
"scipy",
2626
"pandas",
27-
"pynwb",
27+
"pynwb<=3.0.0",
2828
"nwbinspector>=0.5.0",
2929
"ndx_franklab_novela",
3030
"pyyaml",

src/trodes_to_nwb/convert.py

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77

88
import logging
9-
import os
109
from pathlib import Path
1110

1211
import nwbinspector
@@ -18,6 +17,7 @@
1817
from trodes_to_nwb.convert_dios import add_dios
1918
from trodes_to_nwb.convert_ephys import RecFileDataChunkIterator, add_raw_ephys
2019
from trodes_to_nwb.convert_intervals import add_epochs, add_sample_count
20+
from trodes_to_nwb.convert_optogenetics import add_optogenetic_epochs, add_optogenetics
2121
from trodes_to_nwb.convert_position import add_associated_video_files, add_position
2222
from trodes_to_nwb.convert_rec_header import (
2323
add_header_device,
@@ -72,18 +72,16 @@ def setup_logger(name_logfile: str, path_logfile: str) -> logging.Logger:
7272
return logger
7373

7474

75-
def get_included_probe_metadata_paths() -> list[Path]:
75+
def get_included_device_metadata_paths() -> list[Path]:
7676
"""Get the included probe metadata paths
7777
Returns
7878
-------
79-
probe_metadata_paths : list[Path]
79+
device_metadata_paths : list[Path]
8080
List of probe metadata paths
8181
"""
8282
package_dir = Path(__file__).parent.resolve()
83-
probe_folder = package_dir / "probe_metadata"
84-
return [
85-
probe_folder / file for file in probe_folder.iterdir() if file.suffix == ".yml"
86-
]
83+
device_folder = package_dir / "device_metadata"
84+
return device_folder.rglob("*.yml")
8785

8886

8987
def _get_file_paths(df: pd.DataFrame, file_extension: str) -> list[str]:
@@ -107,13 +105,15 @@ def _get_file_paths(df: pd.DataFrame, file_extension: str) -> list[str]:
107105
def create_nwbs(
108106
path: Path,
109107
header_reconfig_path: Path | None = None,
110-
probe_metadata_paths: list[Path] | None = None,
108+
device_metadata_paths: list[Path] | None = None,
111109
output_dir: str = "/stelmo/nwb/raw",
112110
video_directory: str = "",
113111
convert_video: bool = False,
112+
fs_gui_dir: str = "",
114113
n_workers: int = 1,
115114
query_expression: str | None = None,
116115
disable_ptp: bool = False,
116+
behavior_only: bool = False,
117117
):
118118
"""
119119
Convert SpikeGadgets data to NWB format.
@@ -124,14 +124,16 @@ def create_nwbs(
124124
Path to the SpikeGadgets data file.
125125
header_reconfig_path : Path, optional
126126
Path to the header reconfiguration file, by default None.
127-
probe_metadata_paths : list[Path], optional
128-
List of paths to the probe metadata files, by default None.
127+
device_metadata_paths : list[Path], optional
128+
List of paths to the device metadata files, by default None.
129129
output_dir : str, optional
130130
Output directory for the NWB files, by default "/stelmo/nwb/raw".
131131
video_directory : str, optional
132132
Directory containing the video files, by default "".
133133
convert_video : bool, optional
134134
Whether to convert the video files, by default False.
135+
fs_gui_dir : str, optional
136+
Optional alternative directory to find optogenetic files, by default "".
135137
n_workers : int, optional
136138
Number of workers to use for parallel processing, by default 1.
137139
query_expression : str, optional
@@ -140,15 +142,18 @@ def create_nwbs(
140142
See https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html.
141143
disable_ptp : bool, optional
142144
Blocks use of ptp timestamps regardless of rec header, by default False.
145+
behavior_only : bool, optional
146+
Flag to indicate only behaviorsl data (no ephys) was collected in the rec
147+
files, by default False.
143148
144149
"""
145150

146151
if not isinstance(path, Path):
147152
path = Path(path)
148153

149154
# provide the included probe metadata files if none are provided
150-
if probe_metadata_paths is None:
151-
probe_metadata_paths = get_included_probe_metadata_paths()
155+
if device_metadata_paths is None:
156+
device_metadata_paths = get_included_device_metadata_paths()
152157

153158
file_info = get_file_info(path)
154159

@@ -164,10 +169,13 @@ def pass_func(args):
164169
session,
165170
session_df,
166171
header_reconfig_path,
167-
probe_metadata_paths,
172+
device_metadata_paths,
168173
output_dir,
169174
video_directory,
170175
convert_video,
176+
fs_gui_dir,
177+
disable_ptp,
178+
behavior_only=behavior_only,
171179
)
172180
return True
173181
except Exception as e:
@@ -191,23 +199,27 @@ def pass_func(args):
191199
session,
192200
session_df,
193201
header_reconfig_path,
194-
probe_metadata_paths,
202+
device_metadata_paths,
195203
output_dir,
196204
video_directory,
197205
convert_video,
206+
fs_gui_dir,
198207
disable_ptp,
208+
behavior_only=behavior_only,
199209
)
200210

201211

202212
def _create_nwb(
203213
session: tuple[str, str, str],
204214
session_df: pd.DataFrame,
205215
header_reconfig_path: Path | None = None,
206-
probe_metadata_paths: list[Path] | None = None,
216+
device_metadata_paths: list[Path] | None = None,
207217
output_dir: str = "/stelmo/nwb/raw",
208218
video_directory: str = "",
209219
convert_video: bool = False,
220+
fs_gui_dir: str = "",
210221
disable_ptp: bool = False,
222+
behavior_only: bool = False,
211223
):
212224
# create loggers
213225
logger = setup_logger("convert", f"{session[1]}{session[0]}_convert.log")
@@ -219,7 +231,10 @@ def _create_nwb(
219231
logger.info("CREATING REC DATA ITERATORS")
220232
# make generic rec file data chunk iterator to pass to functions
221233
rec_dci = RecFileDataChunkIterator(
222-
rec_filepaths, interpolate_dropped_packets=False, stream_id="trodes"
234+
rec_filepaths,
235+
interpolate_dropped_packets=False,
236+
stream_id="ECU_analog" if behavior_only else "trodes",
237+
behavior_only=behavior_only,
223238
)
224239
rec_dci_timestamps = (
225240
rec_dci.timestamps
@@ -241,8 +256,8 @@ def _create_nwb(
241256
metadata_filepaths = metadata_filepaths[0]
242257
logger.info(f"\tmetadata_filepath: {metadata_filepaths}")
243258

244-
metadata, probe_metadata = load_metadata(
245-
metadata_filepaths, probe_metadata_paths=probe_metadata_paths
259+
metadata, device_metadata = load_metadata(
260+
metadata_filepaths, device_metadata_paths=device_metadata_paths
246261
)
247262

248263
logger.info("CREATING HARDWARE MAPS")
@@ -265,29 +280,36 @@ def _create_nwb(
265280
add_acquisition_devices(nwb_file, metadata)
266281
add_tasks(nwb_file, metadata)
267282
add_associated_files(nwb_file, metadata)
268-
add_electrode_groups(
269-
nwb_file, metadata, probe_metadata, hw_channel_map, ref_electrode_map
270-
)
271283
add_header_device(nwb_file, rec_header)
272284
add_associated_video_files(
273285
nwb_file, metadata, session_df, video_directory, convert_video
274286
)
287+
add_optogenetics(nwb_file, metadata, device_metadata)
275288

276-
logger.info("ADDING EPHYS DATA")
277-
### add rec file data ###
278-
map_row_ephys_data_to_row_electrodes_table = list(
279-
range(len(nwb_file.electrodes))
280-
) # TODO: Double check this
281-
add_raw_ephys(
282-
nwb_file,
283-
rec_filepaths,
284-
map_row_ephys_data_to_row_electrodes_table,
285-
metadata,
286-
)
289+
if not behavior_only:
290+
add_electrode_groups(
291+
nwb_file, metadata, device_metadata, hw_channel_map, ref_electrode_map
292+
)
293+
logger.info("ADDING EPHYS DATA")
294+
# add rec file data
295+
map_row_ephys_data_to_row_electrodes_table = list(
296+
range(len(nwb_file.electrodes))
297+
) # TODO: Double check this
298+
add_raw_ephys(
299+
nwb_file,
300+
rec_filepaths,
301+
map_row_ephys_data_to_row_electrodes_table,
302+
metadata,
303+
)
287304
logger.info("ADDING DIO DATA")
288305
add_dios(nwb_file, rec_filepaths, metadata)
289306
logger.info("ADDING ANALOG DATA")
290-
add_analog_data(nwb_file, rec_filepaths, timestamps=rec_dci_timestamps)
307+
add_analog_data(
308+
nwb_file,
309+
rec_filepaths,
310+
timestamps=rec_dci_timestamps,
311+
behavior_only=behavior_only,
312+
)
291313
logger.info("ADDING SAMPLE COUNTS")
292314
add_sample_count(nwb_file, rec_dci)
293315
logger.info("ADDING EPOCHS")
@@ -296,8 +318,9 @@ def _create_nwb(
296318
session_df=session_df,
297319
neo_io=rec_dci.neo_io,
298320
)
321+
add_optogenetic_epochs(nwb_file, metadata, fs_gui_dir)
299322
logger.info("ADDING POSITION")
300-
### add position ###
323+
# add position
301324
if disable_ptp:
302325
ptp_enabled = False
303326
else:

src/trodes_to_nwb/convert_analog.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515

1616

1717
def add_analog_data(
18-
nwbfile: NWBFile, rec_file_path: list[str], timestamps: np.ndarray = None, **kwargs
18+
nwbfile: NWBFile,
19+
rec_file_path: list[str],
20+
timestamps: np.ndarray = None,
21+
behavior_only: bool = False,
22+
**kwargs,
1923
) -> None:
2024
"""Adds analog streams to the nwb file.
2125
@@ -46,9 +50,10 @@ def add_analog_data(
4650
rec_dci = RecFileDataChunkIterator(
4751
rec_file_path,
4852
nwb_hw_channel_order=analog_channel_ids,
49-
stream_index=2,
53+
stream_id="ECU_analog",
5054
is_analog=True,
5155
timestamps=timestamps,
56+
behavior_only=behavior_only,
5257
)
5358

5459
# add headstage channel IDs to the list of analog channel IDs

src/trodes_to_nwb/convert_ephys.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __init__(
4848
interpolate_dropped_packets: bool = False,
4949
timestamps=None, # Use this if you already have timestamps from intializing another rec iterator on the same files
5050
save_as_microvolts=True,
51+
behavior_only: bool = False,
5152
**kwargs,
5253
):
5354
"""
@@ -72,6 +73,8 @@ def __init__(
7273
timestamps to use. Can provide efficiency improvements by skipping recalculating timestamps from rec files, by default None
7374
save_as_microvolts : bool, optional
7475
convert the ephys data to microvolts prior to saving if True; keep it in raw ADC units if False
76+
behavior_only : bool, optional
77+
indicate if file contains only behavior data (no e-phys), by default False
7578
kwargs : dict
7679
additional arguments to pass to GenericDataChunkIterator
7780
"""
@@ -98,7 +101,15 @@ def __init__(
98101
# trodes
99102
assert all([neo_io.block_count() == 1 for neo_io in self.neo_io])
100103
assert all([neo_io.segment_count(0) == 1 for neo_io in self.neo_io])
101-
assert all([neo_io.signal_streams_count() == 4 for neo_io in self.neo_io])
104+
assert all(
105+
[
106+
neo_io.signal_streams_count() == 4 - behavior_only
107+
for neo_io in self.neo_io
108+
]
109+
), (
110+
"Unexpected number of signal streams. "
111+
+ "Confirm whether behavior_only is set correctly for this recording"
112+
)
102113

103114
self.block_index = 0
104115
self.seg_index = 0
@@ -117,6 +128,10 @@ def __init__(
117128
else: # if stream id is not provided
118129
stream_id = self.neo_io[0].get_stream_id_from_index(stream_index)
119130

131+
if behavior_only and stream_id == "trodes":
132+
raise ValueError(
133+
"Behavior only recordings do not contain a `trodes` stream"
134+
)
120135
self.stream_id = stream_id
121136
self.stream_index = stream_index
122137

0 commit comments

Comments
 (0)