Skip to content

Commit 8c0f187

Browse files
committed
doc improvements
1 parent 420c616 commit 8c0f187

File tree

1 file changed

+55
-49
lines changed

1 file changed

+55
-49
lines changed

src/probeinterface/neuropixels_tools.py

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,11 @@ def build_neuropixels_probe(probe_part_number: str) -> Probe:
353353
"""
354354
Build a Neuropixels probe with all possible contacts from the probe part number.
355355
356-
This function constructs a complete probe geometry including all physical contacts,
357-
MUX table information, and probe-level metadata. The resulting probe contains ALL
358-
electrodes (e.g., 960 for NP1.0, 1280 for NP2.0), not just the subset that might
359-
be recorded in an actual experiment.
356+
This function constructs a complete probe geometry based on IMEC manufacturer specifications
357+
sourced from Bill Karsh's ProbeTable repository (https://github.com/billkarsh/ProbeTable).
358+
The specifications include contact positions, electrode dimensions, shank geometry, MUX routing
359+
tables, and ADC configurations. The resulting probe contains ALL electrodes (e.g., 960 for
360+
NP1.0, 1280 for NP2.0), not just the subset that might be recorded in an actual experiment.
360361
361362
Parameters
362363
----------
@@ -681,65 +682,67 @@ def write_imro(file: str | Path, probe: Probe):
681682

682683
def read_spikeglx(file: str | Path) -> Probe:
683684
"""
684-
Read probe position for the meta file generated by SpikeGLX
685+
Read probe geometry and configuration from a SpikeGLX metadata file.
685686
686-
See http://billkarsh.github.io/SpikeGLX/#metadata-guides for implementation.
687-
The x_pitch/y_pitch/width are set automatically depending on the NP version.
688-
689-
The shape is auto generated as a shank.
687+
This function reconstructs the probe used in a recording by:
688+
1. Reading the probe part number from metadata
689+
2. Building the full probe geometry from manufacturer specifications
690+
3. Slicing to the electrodes selected in the IMRO table
691+
4. Further slicing to channels actually saved to disk (if subset was saved)
692+
5. Adding recording-specific annotations
693+
6. Add wiring (device channel indices)
690694
691695
Parameters
692696
----------
693697
file : Path or str
694-
The .meta file path
698+
Path to the SpikeGLX .meta file
695699
696700
Returns
697701
-------
698-
probe : Probe object
702+
probe : Probe
703+
Probe object with geometry, contact annotations, and device channel mapping
704+
705+
See Also
706+
--------
707+
http://billkarsh.github.io/SpikeGLX/#metadata-guides
699708
700709
"""
701710

702711
meta_file = Path(file)
703712
assert meta_file.suffix == ".meta", "'meta_file' should point to the .meta SpikeGLX file"
704713

705714
meta = parse_spikeglx_meta(meta_file)
706-
707715
assert "imroTbl" in meta, "Could not find imroTbl field in meta file!"
708-
imro_table = meta["imroTbl"]
709716

710-
# read serial number
711-
imDatPrb_serial_number = meta.get("imDatPrb_sn", None)
712-
if imDatPrb_serial_number is None: # this is for Phase3A
713-
imDatPrb_serial_number = meta.get("imProbeSN", None)
714-
715-
# read other metadata
717+
# ===== 1. Extract probe part number from metadata =====
716718
imDatPrb_pn = meta.get("imDatPrb_pn", None)
717-
imDatPrb_port = meta.get("imDatPrb_port", None)
718-
imDatPrb_slot = meta.get("imDatPrb_slot", None)
719-
imDatPrb_part_number = meta.get("imDatPrb_pn", None)
720-
721-
# Only Phase3a probe has "imProbeOpt". Map this to NP10101.
719+
# Only Phase3a probe has "imProbeOpt". Map this to NP1010.
722720
if meta.get("imProbeOpt") is not None:
723721
imDatPrb_pn = "NP1010"
724722

725-
# ===== Step 1: Build full probe with all contacts =====
726-
full_probe = build_neuropixels_probe(imDatPrb_pn)
723+
# ===== 2. Build full probe with all possible contacts =====
724+
# This creates the complete probe geometry (e.g., 960 contacts for NP1.0)
725+
# based on manufacturer specifications
726+
full_probe = build_neuropixels_probe(probe_part_number=imDatPrb_pn)
727727

728-
# ===== Step 2: Parse IMRO table to get active electrode IDs and settings =====
728+
# ===== 3. Parse IMRO table to extract recorded electrodes and acquisition settings =====
729+
# The IMRO table specifies which electrodes were selected for recording (e.g., 384 of 960),
730+
# plus their acquisition settings (gains, references, filters)
731+
imro_table = meta["imroTbl"]
729732
probe_type_num_chans, *imro_table_values_list, _ = imro_table.strip().split(")")
730733

731734
# probe_type_num_chans looks like f"({probe_type},{num_chans}"
732735
probe_type = probe_type_num_chans.split(",")[0][1:]
733736

734737
probe_features = _load_np_probe_features()
735-
_, fields, _ = get_probe_metadata_from_probe_features(probe_features, imDatPrb_pn)
738+
_, imro_fields, _ = get_probe_metadata_from_probe_features(probe_features, imDatPrb_pn)
736739

737740
# Parse IMRO table values
738-
contact_info = {k: [] for k in fields}
741+
contact_info = {k: [] for k in imro_fields}
739742
for field_values_str in imro_table_values_list: # Imro table values look like '(value, value, value, ... '
740743
# Split them by space to get int('value'), int('value'), int('value'), ...)
741744
values = tuple(map(int, field_values_str[1:].split(" ")))
742-
for field, field_value in zip(fields, values):
745+
for field, field_value in zip(imro_fields, values):
743746
contact_info[field].append(field_value)
744747

745748
# Extract electrode IDs from IMRO table
@@ -754,8 +757,8 @@ def read_spikeglx(file: str | Path) -> Probe:
754757
banks = np.array(contact_info[bank_key])
755758
elec_ids = banks * 384 + channel_ids
756759

757-
# ===== Step 3: Slice full probe to active electrodes =====
758-
# Find indices in full probe that match the active electrode IDs from IMRO
760+
# ===== 4. Slice full probe to IMRO-selected electrodes =====
761+
# Match the electrode IDs from IMRO table to contacts in the full probe
759762
keep_indices = []
760763
for elec_id in elec_ids:
761764
# For multi-shank probes, we need to check if shank info is in contact_info
@@ -773,35 +776,38 @@ def read_spikeglx(file: str | Path) -> Probe:
773776

774777
probe = full_probe.get_slice(np.array(keep_indices, dtype=int))
775778

776-
# ===== Step 4: Add IMRO-specific annotations =====
777-
# scalar annotations
779+
# Add IMRO-specific contact annotations (acquisition settings)
778780
probe.annotate(probe_type=probe_type)
779-
780-
# vector annotations
781781
vector_properties = ("channel", "bank", "bank_mask", "ref_id", "ap_gain", "lf_gain", "ap_hipas_flt")
782-
783782
vector_properties_available = {}
784783
for k, v in contact_info.items():
785784
if (k in vector_properties) and (len(v) > 0):
786-
# convert to ProbeInterface naming for backwards compatibility
787785
vector_properties_available[imro_field_to_pi_field.get(k)] = v
788-
789786
probe.annotate_contacts(**vector_properties_available)
790787

791-
# add serial number and other annotations
788+
# ===== 5. Slice to saved channels (if subset was saved) =====
789+
# This is DIFFERENT from IMRO selection: IMRO selects which electrodes to acquire,
790+
# but SpikeGLX can optionally save only a subset of acquired channels to reduce file size.
791+
# For example: IMRO selects 384 electrodes, but only 300 are saved to disk.
792+
saved_chans = get_saved_channel_indices_from_spikeglx_meta(meta_file)
793+
saved_chans = saved_chans[saved_chans < probe.get_contact_count()] # Remove SYS channels
794+
if saved_chans.size != probe.get_contact_count():
795+
probe = probe.get_slice(saved_chans)
796+
797+
# ===== 6. Add recording-specific annotations =====
798+
# These annotations identify the physical probe instance and recording setup
799+
imDatPrb_serial_number = meta.get("imDatPrb_sn") or meta.get("imProbeSN") # Phase3A uses imProbeSN
800+
imDatPrb_port = meta.get("imDatPrb_port", None)
801+
imDatPrb_slot = meta.get("imDatPrb_slot", None)
792802
probe.annotate(serial_number=imDatPrb_serial_number)
793-
probe.annotate(part_number=imDatPrb_part_number)
803+
probe.annotate(part_number=imDatPrb_pn)
794804
probe.annotate(port=imDatPrb_port)
795805
probe.annotate(slot=imDatPrb_slot)
796806

797-
# sometimes we need to slice the probe when not all channels are saved
798-
saved_chans = get_saved_channel_indices_from_spikeglx_meta(meta_file)
799-
# remove the SYS chans
800-
saved_chans = saved_chans[saved_chans < probe.get_contact_count()]
801-
if saved_chans.size != probe.get_contact_count():
802-
# slice if needed
803-
probe = probe.get_slice(saved_chans)
804-
# wire it
807+
# ===== 7. Set device channel indices (wiring) =====
808+
# Device channel indices map probe contacts to data file channels.
809+
# After all slicing, channels are numbered 0, 1, 2, ... N-1 in the order they appear
810+
# in the binary data file, so we wire them sequentially.
805811
probe.set_device_channel_indices(np.arange(probe.get_contact_count()))
806812

807813
return probe

0 commit comments

Comments
 (0)