@@ -130,16 +130,18 @@ def __init__(self, dirname="", load_sync_channel=False, experiment_names=None):
130130 def _source_name (self ):
131131 return self .dirname
132132
133-
134133 def _parse_header (self ):
135134 # Use the static private methods directly
136- folder_structure_dict , possible_experiments = self ._parse_folder_structure (self .dirname , self .experiment_names )
137-
135+ folder_structure_dict , possible_experiments = OpenEphysBinaryRawIO ._parse_folder_structure (
136+ self .dirname , self .experiment_names
137+ )
138138 check_folder_consistency (folder_structure_dict , possible_experiments )
139139 self .folder_structure = folder_structure_dict
140140
141141 # Map folder structure to Neo indexing
142- all_streams , nb_block , nb_segment_per_block = self ._map_folder_structure_to_neo (folder_structure_dict )
142+ all_streams , nb_block , nb_segment_per_block = OpenEphysBinaryRawIO ._map_folder_structure_to_neo (
143+ open_ephys_folder_structure_dict = folder_structure_dict
144+ )
143145
144146 # all streams are consistent across blocks and segments.
145147 # also checks that 'continuous' and 'events' folder are present
@@ -690,22 +692,91 @@ def _get_analogsignal_buffer_description(self, block_index, seg_index, buffer_id
690692 def _parse_folder_structure (dirname , experiment_names = None ):
691693 """
692694 Parse the OpenEphys folder structure by scanning for recordings.
693-
694- This is a private method that handles the core folder discovery logic.
695-
695+
696+ This method walks through the directory tree looking for structure.oebin files,
697+ then builds a hierarchical dictionary that mirrors the OpenEphys folder organization.
698+ It extracts metadata from each structure.oebin file and organizes it by hardware
699+ nodes, experiments, and recordings.
700+
696701 Parameters
697702 ----------
698703 dirname : str
699704 Root folder of the OpenEphys dataset
700705 experiment_names : str, list, or None
701706 If multiple experiments are available, select specific experiments.
702-
707+ If None, all experiments are discovered and included.
708+
703709 Returns
704710 -------
705- folder_structure : dict
711+ open_ephys_folder_structure_dict : dict
706712 Hierarchical dictionary describing the raw OpenEphys folder structure.
707- possible_experiment_names : list
708- List of all available experiment names found in the folder
713+ Structure: [node_name]["experiments"][exp_id]["recordings"][rec_id]["streams"][stream_type][stream_name] -> <parsed_oebin_info>
714+
715+ Where:
716+ - node_name: str, e.g., "Record Node 102" or "" for single-node recordings
717+ - exp_id: int, experiment number from folder name (1, 2, 3, ...)
718+ - rec_id: int, recording number from folder name (1, 2, 3, ...)
719+ - stream_type: str, either "continuous" or "events"
720+ - stream_name: str, e.g., "AP_band", "LF_band", "TTL_1"
721+ - <stream_metadata>: dict containing stream-specific metadata from structure.oebin plus added file paths
722+
723+ For continuous streams, includes:
724+ {
725+ "channels": [{"channel_name": "CH1", "bit_volts": 0.195, ...}, ...],
726+ "sample_rate": 30000.0,
727+ "raw_filename": "/path/to/continuous.dat",
728+ "dtype": "int16",
729+ "timestamp0": 123456,
730+ "t_start": 0.0
731+ }
732+
733+ For event streams, includes:
734+ {
735+ "channel_name": "TTL_1",
736+ "timestamps_npy": "/path/to/timestamps.npy",
737+ "channels_npy": "/path/to/channels.npy",
738+ "sample_numbers_npy": "/path/to/sample_numbers.npy"
739+ }
740+
741+ possible_experiment_names : list of str
742+ List of all experiment folder names found, naturally sorted (e.g., ["experiment1", "experiment2"])
743+
744+ Examples
745+ --------
746+ For a typical multi-node Neuropixels recording:
747+
748+ ```python
749+ open_ephys_folder_structure_dict, experiments = _parse_folder_structure("/path/to/data")
750+
751+ # Structure shape:
752+ open_ephys_folder_structure_dict = {
753+ "Record Node 102": {
754+ "experiments": {
755+ 1: { # from "experiment1" folder
756+ "name": "experiment1",
757+ "settings_file": Path("/path/settings.xml"),
758+ "recordings": {
759+ 1: { # from "recording1" folder
760+ "name": "recording1",
761+ "streams": {
762+ "continuous": {
763+ "AP_band": <continuous_stream_metadata>,
764+ "LF_band": <continuous_stream_metadata>
765+ },
766+ "events": {
767+ "TTL_1": <event_stream_metadata>
768+ }
769+ }
770+ }
771+ }
772+ }
773+ }
774+ },
775+ "Record Node 103": { ... } # Same structure for additional nodes
776+ }
777+
778+ experiments = ["experiment1", "experiment2"]
779+ ```
709780 """
710781 folder_structure = {}
711782 possible_experiment_names = []
@@ -846,28 +917,101 @@ def _parse_folder_structure(dirname, experiment_names=None):
846917 def _map_folder_structure_to_neo (open_ephys_folder_structure_dict ):
847918 """
848919 Map folder structure to Neo indexing system.
849-
850- Converts the hierarchical folder structure to a flattened dictionary
851- organized by Neo's block_index (experiments) and seg_index (recordings).
852-
920+
921+ This method transforms OpenEphys's native hierarchical organization into Neo's
922+ flattened block/segment indexing system. OpenEphys organizes data by hardware
923+ nodes, experiments, and recordings, while Neo uses a two-level structure
924+ of blocks and segments with numerical indices.
925+
853926 Parameters
854927 ----------
855928 open_ephys_folder_structure_dict : dict
856929 Hierarchical folder structure from _parse_folder_structure()
857-
930+ Structure: [node_name]["experiments"][exp_id]["recordings"][rec_id][stream_type][stream_name]
931+
858932 Returns
859933 -------
860- all_streams : dict
934+ block_segment_streams_dict : dict
861935 Neo-indexed dictionary: [block_index][seg_index][stream_type][stream_name]
936+ Where block_index maps to experiment numbers and seg_index maps to recording numbers
862937 nb_block : int
863- Number of blocks (experiments)
938+ Total number of blocks (experiments) available
864939 nb_segment_per_block : dict
865- Number of segments per block. Keys are block indices.
940+ Number of segments per block. Keys are block indices, values are segment counts
941+
942+ Examples
943+ --------
944+ **Input OpenEphys folder_structure_dict:**
945+
946+ OpenEphys native organization with hardware nodes, experiments, and recordings:
947+
948+ ```python
949+ {
950+ "Record Node 102": { # Hardware device 1
951+ "experiments": {
952+ 1: { # experiment1 folder
953+ "recordings": {
954+ 1: {"streams": {"continuous": {"AP_band": <continuous_stream_metadata>, "LF_band": <continuous_stream_metadata>}}}, # recording1
955+ 2: {"streams": {"continuous": {"AP_band": <continuous_stream_metadata>, "LF_band": <continuous_stream_metadata>}}} # recording2
956+ }
957+ },
958+ 2: { # experiment2 folder
959+ "recordings": {
960+ 1: {"streams": {"continuous": {"AP_band": <continuous_stream_metadata>, "LF_band": <continuous_stream_metadata>}}} # recording1
961+ }
962+ }
963+ }
964+ },
965+ "Record Node 103": { # Hardware device 2 (same structure)
966+ "experiments": {
967+ 1: {"recordings": {1: {"streams": {"continuous": {"AP_band": <continuous_stream_metadata>}}}}},
968+ 2: {"recordings": {1: {"streams": {"continuous": {"AP_band": <continuous_stream_metadata>}}}}}
969+ }
970+ }
971+ }
972+ ```
973+
974+ **Output Neo block_segment_streams_dict:**
975+
976+ Neo's flattened block/segment organization with merged multi-node streams:
977+
978+ ```python
979+ {
980+ 0: { # block_index 0 (experiment1)
981+ 0: { # seg_index 0 (recording1)
982+ "continuous": {
983+ "Record Node 102#AP_band": <continuous_stream_metadata>, # Node prefix added
984+ "Record Node 102#LF_band": <continuous_stream_metadata>,
985+ "Record Node 103#AP_band": <continuous_stream_metadata> # Streams from both nodes merged
986+ }
987+ },
988+ 1: { # seg_index 1 (recording2) - only exists for Record Node 102
989+ "continuous": {
990+ "Record Node 102#AP_band": <continuous_stream_metadata>,
991+ "Record Node 102#LF_band": <continuous_stream_metadata>
992+ }
993+ }
994+ },
995+ 1: { # block_index 1 (experiment2)
996+ 0: { # seg_index 0 (recording1)
997+ "continuous": {
998+ "Record Node 102#AP_band": <continuous_stream_metadata>,
999+ "Record Node 102#LF_band": <continuous_stream_metadata>,
1000+ "Record Node 103#AP_band": <continuous_stream_metadata>
1001+ }
1002+ }
1003+ }
1004+ }
1005+ ```
1006+
1007+ **Other outputs:**
1008+ - `nb_block`: 2 (two experiments total)
1009+ - `nb_segment_per_block`: {0: 2, 1: 1} (experiment1 has 2 recordings, experiment2 has 1)
8661010 """
867- all_streams = {}
1011+ block_segment_streams_dict = {}
8681012 nb_segment_per_block = {}
8691013 record_node_names = list (open_ephys_folder_structure_dict .keys ())
870-
1014+
8711015 # Use first record node to determine number of blocks
8721016 recording_node = open_ephys_folder_structure_dict [record_node_names [0 ]]
8731017 nb_block = len (recording_node ["experiments" ])
@@ -878,21 +1022,21 @@ def _map_folder_structure_to_neo(open_ephys_folder_structure_dict):
8781022 for block_index , _ in enumerate (exp_ids_sorted ):
8791023 experiment = recording_node ["experiments" ][exp_ids_sorted [block_index ]]
8801024 nb_segment_per_block [block_index ] = len (experiment ["recordings" ])
881- if block_index not in all_streams :
882- all_streams [block_index ] = {}
1025+ if block_index not in block_segment_streams_dict :
1026+ block_segment_streams_dict [block_index ] = {}
8831027
8841028 rec_ids_sorted = sorted (list (experiment ["recordings" ].keys ()))
8851029 for seg_index , _ in enumerate (rec_ids_sorted ):
8861030 recording = experiment ["recordings" ][rec_ids_sorted [seg_index ]]
887- if seg_index not in all_streams [block_index ]:
888- all_streams [block_index ][seg_index ] = {}
1031+ if seg_index not in block_segment_streams_dict [block_index ]:
1032+ block_segment_streams_dict [block_index ][seg_index ] = {}
8891033 for stream_type in recording ["streams" ]:
890- if stream_type not in all_streams [block_index ][seg_index ]:
891- all_streams [block_index ][seg_index ][stream_type ] = {}
1034+ if stream_type not in block_segment_streams_dict [block_index ][seg_index ]:
1035+ block_segment_streams_dict [block_index ][seg_index ][stream_type ] = {}
8921036 for stream_name , signal_stream in recording ["streams" ][stream_type ].items ():
893- all_streams [block_index ][seg_index ][stream_type ][stream_name ] = signal_stream
1037+ block_segment_streams_dict [block_index ][seg_index ][stream_type ][stream_name ] = signal_stream
8941038
895- return all_streams , nb_block , nb_segment_per_block
1039+ return block_segment_streams_dict , nb_block , nb_segment_per_block
8961040
8971041
8981042_possible_event_stream_names = (
@@ -943,7 +1087,9 @@ def explore_folder(dirname, experiment_names=None):
9431087 List of all available experiments in the Open Ephys folder
9441088 """
9451089 # Use the static private methods for the implementation
946- folder_structure , possible_experiment_names = OpenEphysBinaryRawIO ._parse_folder_structure (dirname , experiment_names )
1090+ folder_structure , possible_experiment_names = OpenEphysBinaryRawIO ._parse_folder_structure (
1091+ dirname , experiment_names
1092+ )
9471093 all_streams , nb_block , nb_segment_per_block = OpenEphysBinaryRawIO ._map_folder_structure_to_neo (folder_structure )
9481094
9491095 return folder_structure , all_streams , nb_block , nb_segment_per_block , possible_experiment_names
@@ -952,17 +1098,17 @@ def explore_folder(dirname, experiment_names=None):
9521098def check_folder_consistency (folder_structure , possible_experiment_names = None ):
9531099 """
9541100 Validate consistency of the discovered OpenEphys folder structure.
955-
956- Ensures that multi-node recordings have consistent experiment/recording
1101+
1102+ Ensures that multi-node recordings have consistent experiment/recording
9571103 structures and that streams are consistent across segments and blocks.
958-
1104+
9591105 Parameters
9601106 ----------
9611107 folder_structure : dict
9621108 Folder structure from explore_folder()
9631109 possible_experiment_names : list, optional
9641110 Available experiment names for error messages
965-
1111+
9661112 Raises
9671113 ------
9681114 ValueError
0 commit comments