From 6c32c77286190c936737eb2cfe20d3e9556214eb Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 12 Nov 2025 16:29:21 +0100 Subject: [PATCH 1/5] Two side probe integration. --- src/probeinterface/plotting.py | 11 +++++ src/probeinterface/probe.py | 84 ++++++++++++++++++++++++++-------- tests/test_plotting.py | 16 ++++++- tests/test_probe.py | 25 ++++++++++ 4 files changed, 117 insertions(+), 19 deletions(-) diff --git a/src/probeinterface/plotting.py b/src/probeinterface/plotting.py index 2830ca0..7f4169c 100644 --- a/src/probeinterface/plotting.py +++ b/src/probeinterface/plotting.py @@ -102,6 +102,7 @@ def plot_probe( ylims: tuple | None = None, zlims: tuple | None = None, show_channel_on_click: bool = False, + side=None, ): """Plot a Probe object. Generates a 2D or 3D axis, depending on Probe.ndim @@ -138,6 +139,8 @@ def plot_probe( Limits for z dimension show_channel_on_click : bool, default: False If True, the channel information is shown upon click + side : None | "front" | "back + If the probe is two side, then the side must be given otherwise this raises an error. Returns ------- @@ -148,6 +151,14 @@ def plot_probe( """ import matplotlib.pyplot as plt + if probe.contact_sides is not None: + if side is None or side not in ('front', 'back'): + raise ValueError("The probe has two side, you must give which one to plot. plot_probe(probe, side='front'|'back')") + mask = probe.contact_sides == side + probe = probe.get_slice(mask) + probe._contact_sides = None + + if ax is None: if probe.ndim == 2: fig, ax = plt.subplots() diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index fb7ac24..7c17cd9 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -101,6 +101,7 @@ def __init__( self.probe_planar_contour = None # This handles the shank id per contact + # If None then one shank only self._shank_ids = None # This handles the wiring to device : channel index on device side. @@ -112,6 +113,10 @@ def __init__( # This must be unique at Probe AND ProbeGroup level self._contact_ids = None + # Handle contact side for double face probes + # If None then one face only + self._contact_sides = None + # annotation: a dict that contains all meta information about # the probe (name, manufacturor, date of production, ...) self.annotations = dict() @@ -153,6 +158,10 @@ def contact_ids(self): def shank_ids(self): return self._shank_ids + @property + def contact_sides(self): + return self._contact_sides + @property def name(self): return self.annotations.get("name", None) @@ -237,6 +246,8 @@ def get_title(self) -> str: if self.shank_ids is not None: num_shank = self.get_shank_count() txt += f" - {num_shank}shanks" + if self._contact_sides is not None: + txt += f" - 2 sides" return txt def __repr__(self): @@ -291,7 +302,7 @@ def get_shank_count(self) -> int: return n def set_contacts( - self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes=None, contact_ids=None, shank_ids=None + self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes=None, contact_ids=None, shank_ids=None, contact_sides=None ): """Sets contacts to a Probe. @@ -320,16 +331,29 @@ def set_contacts( shank_ids : array[str] | None, default: None Defines the shank ids for the contacts. If None, then these are assigned to a unique Shank. + contact_sides : array[str] | None, default: None + If probe is double sided, defines sides by a vector of ['front' | 'back'] """ positions = np.array(positions) if positions.shape[1] != self.ndim: raise ValueError(f"positions.shape[1]: {positions.shape[1]} and ndim: {self.ndim} do not match!") - # Check for duplicate positions - unique_positions = np.unique(positions, axis=0) - positions_are_not_unique = unique_positions.shape[0] != positions.shape[0] - if positions_are_not_unique: - _raise_non_unique_positions_error(positions) + + if contact_sides is None: + # Check for duplicate positions + unique_positions = np.unique(positions, axis=0) + positions_are_not_unique = unique_positions.shape[0] != positions.shape[0] + if positions_are_not_unique: + _raise_non_unique_positions_error(positions) + else: + # Check for duplicate positions side by side + contact_sides = np.asarray(contact_sides).astype(str) + for side in ("front", "back"): + mask = contact_sides == "font" + unique_positions = np.unique(positions[mask], axis=0) + positions_are_not_unique = unique_positions.shape[0] != positions[mask].shape[0] + if positions_are_not_unique: + _raise_non_unique_positions_error(positions[mask]) self._contact_positions = positions n = positions.shape[0] @@ -355,6 +379,15 @@ def set_contacts( self._shank_ids = np.asarray(shank_ids).astype(str) if self.shank_ids.size != n: raise ValueError(f"shank_ids have wrong size: {self.shanks.ids.size} != {n}") + + if contact_sides is None: + self._contact_sides = contact_sides + else: + self._contact_sides = contact_sides + if self._contact_sides.size != n: + raise ValueError(f"contact_sides have wrong size: {self._contact_sides.ids.size} != {n}") + if not np.all(np.isin(self._contact_sides, ["front", "back"])): + raise ValueError(f"contact_sides must 'front' or 'back'") # shape if isinstance(shapes, str): @@ -592,6 +625,13 @@ def __eq__(self, other): ): return False + if self._contact_sides is None: + if other._contact_sides is not None: + return False + else: + if not np.array_equal(self._contact_sides, other._contact_sides): + return False + # Compare contact_annotations dictionaries if self.contact_annotations.keys() != other.contact_annotations.keys(): return False @@ -842,6 +882,7 @@ def rotate_contacts(self, thetas: float | np.array[float] | list[float]): "device_channel_indices", "_contact_ids", "_shank_ids", + "_contact_sides", ] def to_dict(self, array_as_list: bool = False) -> dict: @@ -895,6 +936,9 @@ def from_dict(d: dict) -> "Probe": plane_axes=d["contact_plane_axes"], shapes=d["contact_shapes"], shape_params=d["contact_shape_params"], + contact_ids=d.get("contact_ids", None), + shank_ids=d.get("shank_ids", None), + contact_sides=d.get("contact_sides", None), ) v = d.get("probe_planar_contour", None) @@ -905,14 +949,6 @@ def from_dict(d: dict) -> "Probe": if v is not None: probe.set_device_channel_indices(v) - v = d.get("shank_ids", None) - if v is not None: - probe.set_shank_ids(v) - - v = d.get("contact_ids", None) - if v is not None: - probe.set_contact_ids(v) - if "annotations" in d: probe.annotate(**d["annotations"]) if "contact_annotations" in d: @@ -955,6 +991,7 @@ def to_numpy(self, complete: bool = False) -> np.array: ... ('shank_ids', 'U64'), ('contact_ids', 'U64'), + ('contact_sides', 'U8'), # The rest is added only if `complete=True` ('device_channel_indices', 'int64', optional), @@ -991,6 +1028,9 @@ def to_numpy(self, complete: bool = False) -> np.array: dtype += [(k, "float64")] dtype += [("shank_ids", "U64"), ("contact_ids", "U64")] + if self._contact_sides is not None: + dtype += [("contact_sides", "U8"), ] + if complete: dtype += [("device_channel_indices", "int64")] dtype += [("si_units", "U64")] @@ -1014,6 +1054,11 @@ def to_numpy(self, complete: bool = False) -> np.array: arr["shank_ids"] = self.shank_ids + if self._contact_sides is not None: + arr["contact_sides"] = self.contact_sides + + + if self.contact_ids is None: arr["contact_ids"] = [""] * self.get_contact_count() else: @@ -1062,6 +1107,7 @@ def from_numpy(arr: np.ndarray) -> "Probe": "contact_shapes", "shank_ids", "contact_ids", + "contact_sides", "device_channel_indices", "radius", "width", @@ -1118,17 +1164,19 @@ def from_numpy(arr: np.ndarray) -> "Probe": else: plane_axes = None - probe.set_contacts(positions=positions, plane_axes=plane_axes, shapes=shapes, shape_params=shape_params) + + shank_ids = arr["shank_ids"] if "shank_ids" in fields else None + contact_sides = arr["contact_sides"] if "contact_sides" in fields else None + + probe.set_contacts(positions=positions, plane_axes=plane_axes, shapes=shapes, shape_params=shape_params, shank_ids=shank_ids, contact_sides=contact_sides) if "device_channel_indices" in fields: dev_channel_indices = arr["device_channel_indices"] if not np.all(dev_channel_indices == -1): probe.set_device_channel_indices(dev_channel_indices) - if "shank_ids" in fields: - probe.set_shank_ids(arr["shank_ids"]) if "contact_ids" in fields: probe.set_contact_ids(arr["contact_ids"]) - + # contact annotations for k in contact_annotation_fields: probe.annotate_contacts(**{k: arr[k]}) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index f19c0d7..008c04d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -50,7 +50,21 @@ def test_plot_probegroup(): plot_probegroup(probegroup_3d, same_axes=True) +def test_plot_probe_two_side(): + probe = Probe() + probe.set_contacts( + positions=np.array([[0, 0], [0, 10], [0, 20],[0, 0], [0, 10], [0, 20],]), + shapes="circle", + contact_ids=["F1", "F2", "F3", "B1", "B2", "B3"], + contact_sides=["front", "front", "front", "back", "back","back"] + ) + + plot_probe(probe, with_contact_id=True, side="front") + plot_probe(probe, with_contact_id=True, side="back") + + if __name__ == "__main__": - test_plot_probe() + # test_plot_probe() # test_plot_probe_group() + test_plot_probe_two_side() plt.show() diff --git a/tests/test_probe.py b/tests/test_probe.py index b20ea0d..255ec78 100644 --- a/tests/test_probe.py +++ b/tests/test_probe.py @@ -197,9 +197,34 @@ def test_position_uniqueness(): probe.set_contacts(positions=positions_with_dups, shapes="circle", shape_params={"radius": 5}) +def test_double_side_probe(): + + probe = Probe() + probe.set_contacts( + positions=np.array([[0, 0], [0, 10], [0, 20],[0, 0], [0, 10], [0, 20],]), + shapes="circle", + contact_sides=["front", "front", "front", "back", "back","back"] + ) + print(probe) + + assert "contact_sides" in probe.to_dict() + + probe2 = Probe.from_dict(probe.to_dict()) + assert probe2 == probe + + probe3 = Probe.from_numpy(probe.to_numpy()) + assert probe3 == probe + + probe4 = Probe.from_dataframe(probe.to_dataframe()) + assert probe4 == probe + + + if __name__ == "__main__": test_probe() tmp_path = Path("tmp") tmp_path.mkdir(exist_ok=True) test_save_to_zarr(tmp_path) + + test_double_side_probe() From 9f26792ba6202565e7cd36f45790c3ed5e7cef72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:35:14 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/probeinterface/plotting.py | 7 ++++--- src/probeinterface/probe.py | 32 ++++++++++++++++++++++---------- tests/test_plotting.py | 13 +++++++++++-- tests/test_probe.py | 18 +++++++++++++----- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/probeinterface/plotting.py b/src/probeinterface/plotting.py index 7f4169c..1c8de07 100644 --- a/src/probeinterface/plotting.py +++ b/src/probeinterface/plotting.py @@ -152,13 +152,14 @@ def plot_probe( import matplotlib.pyplot as plt if probe.contact_sides is not None: - if side is None or side not in ('front', 'back'): - raise ValueError("The probe has two side, you must give which one to plot. plot_probe(probe, side='front'|'back')") + if side is None or side not in ("front", "back"): + raise ValueError( + "The probe has two side, you must give which one to plot. plot_probe(probe, side='front'|'back')" + ) mask = probe.contact_sides == side probe = probe.get_slice(mask) probe._contact_sides = None - if ax is None: if probe.ndim == 2: fig, ax = plt.subplots() diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index 7c17cd9..e048e57 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -302,7 +302,14 @@ def get_shank_count(self) -> int: return n def set_contacts( - self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes=None, contact_ids=None, shank_ids=None, contact_sides=None + self, + positions, + shapes="circle", + shape_params={"radius": 10}, + plane_axes=None, + contact_ids=None, + shank_ids=None, + contact_sides=None, ): """Sets contacts to a Probe. @@ -338,7 +345,6 @@ def set_contacts( if positions.shape[1] != self.ndim: raise ValueError(f"positions.shape[1]: {positions.shape[1]} and ndim: {self.ndim} do not match!") - if contact_sides is None: # Check for duplicate positions unique_positions = np.unique(positions, axis=0) @@ -353,7 +359,7 @@ def set_contacts( unique_positions = np.unique(positions[mask], axis=0) positions_are_not_unique = unique_positions.shape[0] != positions[mask].shape[0] if positions_are_not_unique: - _raise_non_unique_positions_error(positions[mask]) + _raise_non_unique_positions_error(positions[mask]) self._contact_positions = positions n = positions.shape[0] @@ -379,7 +385,7 @@ def set_contacts( self._shank_ids = np.asarray(shank_ids).astype(str) if self.shank_ids.size != n: raise ValueError(f"shank_ids have wrong size: {self.shanks.ids.size} != {n}") - + if contact_sides is None: self._contact_sides = contact_sides else: @@ -1029,7 +1035,9 @@ def to_numpy(self, complete: bool = False) -> np.array: dtype += [("shank_ids", "U64"), ("contact_ids", "U64")] if self._contact_sides is not None: - dtype += [("contact_sides", "U8"), ] + dtype += [ + ("contact_sides", "U8"), + ] if complete: dtype += [("device_channel_indices", "int64")] @@ -1057,8 +1065,6 @@ def to_numpy(self, complete: bool = False) -> np.array: if self._contact_sides is not None: arr["contact_sides"] = self.contact_sides - - if self.contact_ids is None: arr["contact_ids"] = [""] * self.get_contact_count() else: @@ -1164,11 +1170,17 @@ def from_numpy(arr: np.ndarray) -> "Probe": else: plane_axes = None - shank_ids = arr["shank_ids"] if "shank_ids" in fields else None contact_sides = arr["contact_sides"] if "contact_sides" in fields else None - probe.set_contacts(positions=positions, plane_axes=plane_axes, shapes=shapes, shape_params=shape_params, shank_ids=shank_ids, contact_sides=contact_sides) + probe.set_contacts( + positions=positions, + plane_axes=plane_axes, + shapes=shapes, + shape_params=shape_params, + shank_ids=shank_ids, + contact_sides=contact_sides, + ) if "device_channel_indices" in fields: dev_channel_indices = arr["device_channel_indices"] @@ -1176,7 +1188,7 @@ def from_numpy(arr: np.ndarray) -> "Probe": probe.set_device_channel_indices(dev_channel_indices) if "contact_ids" in fields: probe.set_contact_ids(arr["contact_ids"]) - + # contact annotations for k in contact_annotation_fields: probe.annotate_contacts(**{k: arr[k]}) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 008c04d..eaa676f 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -53,10 +53,19 @@ def test_plot_probegroup(): def test_plot_probe_two_side(): probe = Probe() probe.set_contacts( - positions=np.array([[0, 0], [0, 10], [0, 20],[0, 0], [0, 10], [0, 20],]), + positions=np.array( + [ + [0, 0], + [0, 10], + [0, 20], + [0, 0], + [0, 10], + [0, 20], + ] + ), shapes="circle", contact_ids=["F1", "F2", "F3", "B1", "B2", "B3"], - contact_sides=["front", "front", "front", "back", "back","back"] + contact_sides=["front", "front", "front", "back", "back", "back"], ) plot_probe(probe, with_contact_id=True, side="front") diff --git a/tests/test_probe.py b/tests/test_probe.py index 255ec78..48a3b82 100644 --- a/tests/test_probe.py +++ b/tests/test_probe.py @@ -201,25 +201,33 @@ def test_double_side_probe(): probe = Probe() probe.set_contacts( - positions=np.array([[0, 0], [0, 10], [0, 20],[0, 0], [0, 10], [0, 20],]), + positions=np.array( + [ + [0, 0], + [0, 10], + [0, 20], + [0, 0], + [0, 10], + [0, 20], + ] + ), shapes="circle", - contact_sides=["front", "front", "front", "back", "back","back"] + contact_sides=["front", "front", "front", "back", "back", "back"], ) print(probe) assert "contact_sides" in probe.to_dict() probe2 = Probe.from_dict(probe.to_dict()) - assert probe2 == probe + assert probe2 == probe probe3 = Probe.from_numpy(probe.to_numpy()) - assert probe3 == probe + assert probe3 == probe probe4 = Probe.from_dataframe(probe.to_dataframe()) assert probe4 == probe - if __name__ == "__main__": test_probe() From 9df68df155d677b23e33cef5970e29a6605384c3 Mon Sep 17 00:00:00 2001 From: Garcia Samuel Date: Fri, 14 Nov 2025 15:58:49 +0100 Subject: [PATCH 3/5] merci Co-authored-by: Alessio Buccino --- src/probeinterface/plotting.py | 2 +- src/probeinterface/probe.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/probeinterface/plotting.py b/src/probeinterface/plotting.py index 1c8de07..ce535cf 100644 --- a/src/probeinterface/plotting.py +++ b/src/probeinterface/plotting.py @@ -139,7 +139,7 @@ def plot_probe( Limits for z dimension show_channel_on_click : bool, default: False If True, the channel information is shown upon click - side : None | "front" | "back + side : None | "front" | "back" If the probe is two side, then the side must be given otherwise this raises an error. Returns diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index e048e57..f19e9b5 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -355,7 +355,7 @@ def set_contacts( # Check for duplicate positions side by side contact_sides = np.asarray(contact_sides).astype(str) for side in ("front", "back"): - mask = contact_sides == "font" + mask = contact_sides == "front" unique_positions = np.unique(positions[mask], axis=0) positions_are_not_unique = unique_positions.shape[0] != positions[mask].shape[0] if positions_are_not_unique: From abfbb59dc2fd423a2c31f26c884f80d29593458b Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Fri, 12 Dec 2025 12:47:49 +0100 Subject: [PATCH 4/5] Fix mask --- src/probeinterface/probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index f19e9b5..2572708 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -355,7 +355,7 @@ def set_contacts( # Check for duplicate positions side by side contact_sides = np.asarray(contact_sides).astype(str) for side in ("front", "back"): - mask = contact_sides == "front" + mask = contact_sides == side unique_positions = np.unique(positions[mask], axis=0) positions_are_not_unique = unique_positions.shape[0] != positions[mask].shape[0] if positions_are_not_unique: From 6a3fa0b7670c3fdf16127fbb97e49e9fa007aeb4 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 16 Dec 2025 10:56:10 +0100 Subject: [PATCH 5/5] Update generate_cambridge_neurotech script --- resources/CN-logo.jpg | Bin 0 -> 18609 bytes .../generate_cambridgeneurotech_libray.py | 421 ++++++------------ 2 files changed, 134 insertions(+), 287 deletions(-) create mode 100644 resources/CN-logo.jpg diff --git a/resources/CN-logo.jpg b/resources/CN-logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..204a3d5baa80802f7ba8289f7762b6fc7752201e GIT binary patch literal 18609 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 0.2.18 - # target_dict["probes"][0]["annotations"].pop("first_index") - - same = source_dict == target_dict - - # copy the json - shutil.copyfile(source_probe_file, target_probe_file) - if not same: - # copy the png - shutil.copyfile(source_probe_file.parent / (source_probe_file.stem + '.png'), - target_probe_file.parent / (target_probe_file.stem + '.png') ) + wrong_contours = [] + sheets_with_issues = [] + for sheet_name in tqdm(sheet_names, "Exporting CN probes"): + contacts = pd.read_excel(probe_tables_path / "probe_contacts.xlsx", sheet_name=sheet_name) + contour = pd.read_excel(probe_tables_path / "probe_contours.xlsx", sheet_name=sheet_name) + if np.all(pd.isna(contacts["contact_sides"])): + contacts.drop(columns="contact_sides", inplace=True) + else: + print(f"Double sided probe: {sheet_name}") + if "z" in contacts.columns: + contacts.drop(columns=["z"], inplace=True) + try: + probe = Probe.from_dataframe(contacts) + probe.manufacturer = "cambridgeneurotech" + probe.model_name = sheet_name + probe.set_planar_contour(contour) + if not is_contour_correct(probe): + wrong_contours.append(sheet_name) + export_one_probe(sheet_name, probe, output_folder) - # library_folder + except Exception as e: + print(f"Problem loading {sheet_name}: {e}") + sheets_with_issues.append(sheet_name) + print("Wrong contours:\n\n", wrong_contours) + print("Sheets with issues:\n\n", sheets_with_issues) -if __name__ == '__main__': - generate_all_probes() - synchronize_library() +if __name__ == "__main__": + args = parser.parse_args() + probe_tables_path = Path(args.probe_tables_path) + output_folder = Path(args.output_folder) + if output_folder.exists(): + shutil.rmtree(output_folder) + output_folder.mkdir(parents=True, exist_ok=True) + generate_all_probes(probe_tables_path, output_folder)