Skip to content

Commit 69695b2

Browse files
authored
Add static method to NWBRecordingExtractor to fetch the available electrical series (#2903)
* add static methods to find out available electrical series * rename * yet another rename
1 parent d81fb7f commit 69695b2

2 files changed

Lines changed: 77 additions & 12 deletions

File tree

src/spikeinterface/extractors/nwbextractors.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def read_file_from_backend(
8686
else:
8787
import h5py
8888

89-
assert file is not None, "Unexpected, file is None"
89+
assert file is not None, "Both file_path and file are None"
9090
open_file = h5py.File(file, "r")
9191

9292
return open_file
@@ -124,11 +124,6 @@ def read_nwbfile(
124124
nwbfile : NWBFile
125125
The NWBFile object.
126126
127-
Raises
128-
------
129-
AssertionError
130-
If ROS3 support is not enabled.
131-
132127
Notes
133128
-----
134129
This function can stream data from the "fsspec", and "rem" protocols.
@@ -276,14 +271,17 @@ def _retrieve_unit_table_pynwb(nwbfile: "NWBFile", unit_table_path: Optional[str
276271

277272

278273
def _is_hdf5_file(filename_or_file):
279-
# Source for magic numbers https://www.loc.gov/preservation/digital/formats/fdd/fdd000229.shtml
280-
# We should find a better one though
281274
if isinstance(filename_or_file, (str, Path)):
282-
with open(filename_or_file, "rb") as f:
283-
file_signature = f.read(8)
275+
import h5py
276+
277+
filename = str(filename_or_file)
278+
is_hdf5 = h5py.h5f.is_hdf5(filename.encode("utf-8"))
284279
else:
285280
file_signature = filename_or_file.read(8)
286-
return file_signature == b"\x89HDF\r\n\x1a\n"
281+
# Source of the magic number https://docs.hdfgroup.org/hdf5/develop/_f_m_t3.html
282+
is_hdf5 = file_signature == b"\x89HDF\r\n\x1a\n"
283+
284+
return is_hdf5
287285

288286

289287
def _get_backend_from_local_file(file_path: str | Path) -> str:
@@ -861,6 +859,57 @@ def _fetch_main_properties_backend(self):
861859

862860
return gains, offsets, locations, groups
863861

862+
@staticmethod
863+
def fetch_available_electrical_series_paths(
864+
file_path: str | Path, stream_mode: Optional[Literal["fsspec", "remfile", "zarr"]] = None
865+
) -> List[str]:
866+
"""
867+
Retrieves the paths to all ElectricalSeries objects within a neurodata file.
868+
869+
Parameters
870+
----------
871+
file_path : str | Path
872+
The path to the neurodata file to be analyzed.
873+
stream_mode : "fsspec" | "remfile" | "zarr" | None, optional
874+
Determines the streaming mode for reading the file. Use this for optimized reading from
875+
different sources, such as local disk or remote servers.
876+
877+
Returns
878+
-------
879+
list of str
880+
A list of paths to all ElectricalSeries objects found in the file.
881+
882+
883+
Notes
884+
-----
885+
The paths are returned as strings, and can be used to load the desired ElectricalSeries object.
886+
Examples of paths are:
887+
- "acquisition/ElectricalSeries1"
888+
- "acquisition/ElectricalSeries2"
889+
- "processing/ecephys/LFP/ElectricalSeries1"
890+
- "processing/my_custom_module/MyContainer/ElectricalSeries2"
891+
"""
892+
893+
if stream_mode is None:
894+
backend = _get_backend_from_local_file(file_path)
895+
else:
896+
if stream_mode == "zarr":
897+
backend = "zarr"
898+
else:
899+
backend = "hdf5"
900+
901+
file_handle = read_file_from_backend(
902+
file_path=file_path,
903+
stream_mode=stream_mode,
904+
)
905+
906+
electrical_series_paths = _find_neurodata_type_from_backend(
907+
file_handle,
908+
neurodata_type="ElectricalSeries",
909+
backend=backend,
910+
)
911+
return electrical_series_paths
912+
864913

865914
class NwbRecordingSegment(BaseRecordingSegment):
866915
def __init__(self, electrical_series_data, times_kwargs):

src/spikeinterface/extractors/tests/test_nwbextractors.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,22 @@ def test_retrieving_from_processing(generate_nwbfile, use_pynwb):
319319
assert np.array_equal(electrical_series_custom.data[:], recording_extractor_custom.get_traces())
320320

321321

322+
def test_fetch_available_electrical_series_paths(generate_nwbfile):
323+
path_to_nwbfile, _ = generate_nwbfile
324+
available_electrical_series = NwbRecordingExtractor.fetch_available_electrical_series_paths(
325+
file_path=path_to_nwbfile
326+
)
327+
328+
expected_paths = [
329+
"acquisition/ElectricalSeries1",
330+
"acquisition/ElectricalSeries2",
331+
"processing/ecephys/LFP/ElectricalSeries1",
332+
"processing/my_custom_module/MyContainer/ElectricalSeries2",
333+
]
334+
335+
assert available_electrical_series == expected_paths
336+
337+
322338
@pytest.mark.parametrize("electrical_series_path", ["acquisition/ElectricalSeries1", "acquisition/ElectricalSeries2"])
323339
def test_recording_equality_with_pynwb_and_backend(generate_nwbfile, electrical_series_path):
324340
path_to_nwbfile, _ = generate_nwbfile
@@ -338,7 +354,7 @@ def test_recording_equality_with_pynwb_and_backend(generate_nwbfile, electrical_
338354

339355

340356
@pytest.mark.parametrize("use_pynwb", [True, False])
341-
def test_failure_with_wrong_electrical_series_name(generate_nwbfile, use_pynwb):
357+
def test_failure_with_wrong_electrical_series_path(generate_nwbfile, use_pynwb):
342358
"""Test that the extractor raises an error if the electrical series name is not found."""
343359
path_to_nwbfile, _ = generate_nwbfile
344360
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)