Skip to content

Commit 8c0a3bc

Browse files
authored
Enhance ProbeGroup API and add ProbeGroup._global_contact_order for interleaved contacts (#446)
1 parent 7aa8ace commit 8c0a3bc

7 files changed

Lines changed: 776 additions & 133 deletions

File tree

examples/ex_03_generate_probe_group.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,65 @@
4646

4747
plot_probegroup(probegroup, same_axes=False, with_contact_id=True)
4848

49+
##############################################################################
50+
# Identifying probes with a ``probe_id``
51+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
52+
#
53+
# Each probe in a `ProbeGroup` can be given a human-readable ``probe_id`` when
54+
# it is added. This is handy to keep track of which probe targets which brain
55+
# area or hemisphere. If no ``probe_id`` is given, a default one
56+
# (``"probe_1"``, ``"probe_2"``, ...) is generated automatically.
57+
58+
probe0 = generate_dummy_probe(elec_shapes='square')
59+
probe1 = generate_dummy_probe(elec_shapes='circle')
60+
probe1.move([250, -90])
61+
62+
probegroup = ProbeGroup()
63+
probegroup.add_probe(probe0, probe_id="left_hemisphere")
64+
probegroup.add_probe(probe1, probe_id="right_hemisphere")
65+
66+
print(probegroup)
67+
print("probe_ids:", probegroup.probe_ids)
68+
69+
##############################################################################
70+
# `ProbeGroup.select_probes()` returns a new `ProbeGroup` with a sub-selection
71+
# of probes given by probe_ids.
72+
73+
left_hemisphere_probe = probegroup.select_probes(probe_ids=["left_hemisphere"])
74+
print(left_hemisphere_probe)
75+
76+
##############################################################################
77+
# We can also select by specific contacts from a probegroup with the
78+
# ``select_contacts`` function. Note that if ``contact_ids`` are not
79+
# unique across probes, you need to disambiguate the selection by specifying the
80+
# probe_ids as well. Otherwise, a ValueError is raised.
81+
82+
# check if any contact_id is not unique across probes
83+
contact_ids = probegroup.get_global_contact_ids()
84+
if len(contact_ids) != len(set(contact_ids)):
85+
print("contact_ids are not unique across probes, you should provide probe_ids to disambiguate")
86+
87+
##############################################################################
88+
# Because the contact ids are not unique across probes, combining ``contact_ids``
89+
# with ``probe_ids`` lets us pull specific contacts from a single hemisphere:
90+
91+
left_probegroup = probegroup.select_contacts(
92+
contact_ids=["0", "1", "2"],
93+
probe_ids=["left_hemisphere", "left_hemisphere", "left_hemisphere"]
94+
)
95+
print(left_probegroup)
96+
97+
# Now select contacts from both hemispheres by providing the corresponding probe_ids for each contact_id:
98+
left_and_right_probegroup = probegroup.select_contacts(
99+
contact_ids=["0", "1", "2"],
100+
probe_ids=["left_hemisphere", "right_hemisphere", "left_hemisphere"]
101+
)
102+
print(left_and_right_probegroup)
103+
104+
# Without providing probe_ids, the selection is ambiguous and an error is raised:
105+
try:
106+
ambiguous_selection = probegroup.select_contacts(contact_ids=["0", "1", "2"])
107+
except ValueError as e:
108+
print("Error raised for ambiguous selection:", e)
109+
49110
plt.show()

src/probeinterface/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353
cache_full_library,
5454
clear_cache,
5555
)
56-
from .wiring import get_available_pathways
56+
from .wiring import get_available_pathways, get_pathway, wire_probe

src/probeinterface/io.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,9 @@ def read_BIDS_probe(folder: str | Path, prefix: str | None = None) -> ProbeGroup
199199

200200
# create probe object and register with probegroup
201201
probe = Probe.from_dataframe(df=df_probe)
202-
probe.annotate(probe_id=probe_id)
203202

204203
probes[str(probe_id)] = probe
205-
probegroup.add_probe(probe)
204+
probegroup.add_probe(probe, probe_id=str(probe_id))
206205

207206
ignore_annotations = [
208207
"probe_ids",
@@ -322,7 +321,7 @@ def write_BIDS_probe(folder: str | Path, probe_or_probegroup: Probe | ProbeGroup
322321
probegroup = probe_or_probegroup
323322
else:
324323
raise TypeError(
325-
f"probe_or_probegroup has to be" "of type Probe or ProbeGroup " f"not type: {type(probe_or_probegroup)}"
324+
f"probe_or_probegroup has to be of type Probe or ProbeGroup not type: {type(probe_or_probegroup)}"
326325
)
327326
folder = Path(folder)
328327

@@ -333,22 +332,12 @@ def write_BIDS_probe(folder: str | Path, probe_or_probegroup: Probe | ProbeGroup
333332
probes = probegroup.probes
334333

335334
# Step 1: GENERATION OF PROBE.TSV
336-
# ensure required keys (probe_id, probe_type) are present
337-
338-
if any("probe_id" not in p.annotations for p in probes):
339-
probegroup.auto_generate_probe_ids()
335+
# ensure required keys (probe_type) are present
340336

341337
for probe in probes:
342-
if "probe_id" not in probe.annotations:
343-
raise ValueError(
344-
"Export to BIDS probe format requires "
345-
"the probe id to be specified as an annotation "
346-
"(probe_id). You can do this via "
347-
"`probegroup.auto_generate_ids."
348-
)
349338
if "type" not in probe.annotations:
350339
raise ValueError(
351-
"Export to BIDS probe format requires " "the probe type to be specified as an " "annotation (type)"
340+
"Export to BIDS probe format requires the probe type to be specified as an annotation (type)"
352341
)
353342

354343
# extract all used annotation keys
@@ -357,11 +346,12 @@ def write_BIDS_probe(folder: str | Path, probe_or_probegroup: Probe | ProbeGroup
357346
annotation_keys = np.unique(keys_concatenated)
358347

359348
# generate a tsv table capturing probe information
360-
index = range(len([p.annotations["probe_id"] for p in probes]))
349+
index = range(len(probes))
361350
df = pd.DataFrame(index=index)
362351
for annotation_key in annotation_keys:
363352
df[annotation_key] = [p.annotations[annotation_key] for p in probes]
364353
df["n_shanks"] = [len(np.unique(p.shank_ids)) for p in probes]
354+
df["probe_id"] = probegroup.probe_ids
365355

366356
# Note: in principle it would also be possible to add the probe width and
367357
# depth here based on the probe contour information. However this would
@@ -374,8 +364,7 @@ def write_BIDS_probe(folder: str | Path, probe_or_probegroup: Probe | ProbeGroup
374364

375365
# Step 2: GENERATION OF PROBE.JSON
376366
probes_dict = {}
377-
for probe in probes:
378-
probe_id = probe.annotations["probe_id"]
367+
for probe_id, probe in zip(probegroup.probe_ids, probes):
379368
probes_dict[probe_id] = {
380369
"contour": probe.probe_planar_contour.tolist(),
381370
"units": probe.si_units,
@@ -399,7 +388,7 @@ def write_BIDS_probe(folder: str | Path, probe_or_probegroup: Probe | ProbeGroup
399388
index = range(sum([p.get_contact_count() for p in probes]))
400389
df.rename(columns=tsv_label_map_to_BIDS, inplace=True)
401390

402-
df["probe_id"] = [p.annotations["probe_id"] for p in probes for _ in p.contact_ids]
391+
df["probe_id"] = [probe_id for probe_id, probe in zip(probegroup.probe_ids, probes) for _ in probe.contact_ids]
403392
df["coordinate_system"] = ["relative cartesian"] * len(index)
404393

405394
channel_indices = []

src/probeinterface/probe.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def set_device_channel_indices(self, channel_indices: np.ndarray | list):
534534
)
535535
self.device_channel_indices = channel_indices
536536
if self._probe_group is not None:
537-
self._probe_group.check_global_device_wiring_and_ids()
537+
self._probe_group._check_global_device_wiring_and_ids()
538538

539539
def wiring_to_device(self, pathway: str, channel_offset: int = 0):
540540
"""
@@ -584,7 +584,7 @@ def set_contact_ids(self, contact_ids: np.ndarray | list):
584584

585585
self._contact_ids = contact_ids
586586
if self._probe_group is not None:
587-
self._probe_group.check_global_device_wiring_and_ids()
587+
self._probe_group._check_global_device_wiring_and_ids()
588588

589589
def set_shank_ids(self, shank_ids: np.ndarray | list):
590590
"""
@@ -1140,8 +1140,10 @@ def from_numpy(arr: np.ndarray) -> "Probe":
11401140
"plane_axis_y_1",
11411141
"plane_axis_z_0",
11421142
"plane_axis_z_1",
1143-
"probe_index",
11441143
"si_units",
1144+
# these two are for ProbeGroup to avoid duplication of fields
1145+
"probe_index",
1146+
"probe_id",
11451147
]
11461148
contact_annotation_fields = [f for f in fields if f not in main_fields]
11471149

0 commit comments

Comments
 (0)