Skip to content

Commit 2cec18e

Browse files
authored
Merge branch 'main' into separate_spikeglx_catalogue_from_recording_probe
2 parents 44752fc + 47182d4 commit 2cec18e

8 files changed

Lines changed: 335 additions & 358 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ repos:
66
- id: end-of-file-fixer
77
- id: trailing-whitespace
88
- repo: https://github.com/psf/black-pre-commit-mirror
9-
rev: 25.11.0
9+
rev: 25.12.0
1010
hooks:
1111
- id: black
1212
files: ^src/|^tests/

resources/CN-logo.jpg

18.2 KB
Loading
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
2025-12-16 CambridgeNeurotech
3+
4+
Derive probes to be used with SpikeInterface base on Cambridgeneurotech database at:
5+
https://github.com/cambridge-neurotech/probe_maps
6+
7+
8+
The output folder is ready to be used as a probeinterface library and contains:
9+
- one folder per probe
10+
- inside each folder a json file and a figure png file
11+
"""
12+
13+
import argparse
14+
import json
15+
import shutil
16+
from pathlib import Path
17+
import pandas as pd
18+
import numpy as np
19+
import matplotlib.pyplot as plt
20+
from tqdm.auto import tqdm
21+
22+
from probeinterface.plotting import plot_probe
23+
from probeinterface import write_probeinterface, Probe
24+
25+
26+
cn_logo = Path(__file__).parent / "CN-logo.jpg"
27+
28+
parser = argparse.ArgumentParser(description="Generate CambridgeNeurotech probe library for probeinterface")
29+
parser.add_argument(
30+
"probe_tables_path",
31+
type=str,
32+
help="Path to the folder containing the CambridgeNeurotech probe tables CSV files from https://github.com/cambridge-neurotech/probe_maps",
33+
)
34+
parser.add_argument(
35+
"--output-folder", type=str, default="./cambridgeneurotech", help="Output folder to save the generated probes"
36+
)
37+
38+
39+
# graphing parameters
40+
plt.rcParams["pdf.fonttype"] = 42 # to make sure it is recognize as true font in illustrator
41+
plt.rcParams["svg.fonttype"] = "none" # to make sure it is recognize as true font in illustrator
42+
43+
44+
def create_CN_figure(probe):
45+
"""
46+
Create custom figire for CN with custom colors + logo
47+
"""
48+
if probe.contact_sides is not None:
49+
fig, axs = plt.subplots(ncols=2)
50+
fig.set_size_inches(18.5, 10.5)
51+
else:
52+
fig, ax = plt.subplots()
53+
fig.set_size_inches(18.5, 10.5)
54+
axs = [ax]
55+
56+
n = probe.get_contact_count()
57+
probe_max_height = np.max(probe.contact_positions[:, 1])
58+
if probe.contact_sides is not None:
59+
for i, side in enumerate(("front", "back")):
60+
ax = axs[i]
61+
plot_probe(
62+
probe,
63+
ax=ax,
64+
contacts_colors=["#5bc5f2"] * n, # made change to default color
65+
probe_shape_kwargs=dict(
66+
facecolor="#6f6f6e", edgecolor="k", lw=0.5, alpha=0.3
67+
), # made change to default color
68+
with_contact_id=True,
69+
side=side,
70+
)
71+
ax.set_title(f"Side: {side}", fontsize=20)
72+
else:
73+
plot_probe(
74+
probe,
75+
ax=axs[0],
76+
contacts_colors=["#5bc5f2"] * n, # made change to default color
77+
probe_shape_kwargs=dict(
78+
facecolor="#6f6f6e", edgecolor="k", lw=0.5, alpha=0.3
79+
), # made change to default color
80+
with_contact_id=True,
81+
)
82+
axs[0].set_title("")
83+
84+
for ax in axs:
85+
y_min = ax.get_ylim()[0]
86+
y_max = probe_max_height + 200
87+
ax.set_ylim(y_min, y_max)
88+
ax.set_xlabel("Width (\u03bcm)") # modify to legend
89+
ax.set_ylabel("Height (\u03bcm)") # modify to legend
90+
ax.spines["right"].set_visible(False) # remove external axis
91+
ax.spines["top"].set_visible(False) # remove external axis
92+
93+
fig.suptitle("\n" + "CambridgeNeuroTech" + "\n" + probe.model_name, fontsize=24)
94+
95+
fig.tight_layout()
96+
97+
im = plt.imread(str(cn_logo))
98+
newax = fig.add_axes([0.8, 0.85, 0.2, 0.1], anchor="NW", zorder=0)
99+
newax.imshow(im)
100+
newax.axis("off")
101+
102+
return fig
103+
104+
105+
def export_one_probe(probe_name, probe, output_folder):
106+
"""
107+
Save one probe in "output_folder" + figure.
108+
"""
109+
probe_folder = output_folder / probe_name
110+
probe_folder.mkdir(exist_ok=True, parents=True)
111+
probe_file = probe_folder / (probe_name + ".json")
112+
figure_file = probe_folder / (probe_name + ".png")
113+
114+
write_probeinterface(probe_file, probe)
115+
116+
fig = create_CN_figure(probe)
117+
fig.savefig(figure_file)
118+
119+
plt.close(fig)
120+
121+
122+
def is_contour_correct(probe):
123+
from shapely.geometry import Point, Polygon
124+
125+
polygon = Polygon(probe.probe_planar_contour)
126+
127+
for i, contact_pos in enumerate(probe.contact_positions):
128+
width = probe.contact_shape_params[i]["width"]
129+
height = probe.contact_shape_params[i]["height"]
130+
points = [
131+
(contact_pos[0] - width / 2, contact_pos[1] - height / 2),
132+
(contact_pos[0] + width / 2, contact_pos[1] - height / 2),
133+
(contact_pos[0] + width / 2, contact_pos[1] + height / 2),
134+
(contact_pos[0] - width / 2, contact_pos[1] + height / 2),
135+
]
136+
for point in points:
137+
p = Point(point[0], point[1])
138+
if not polygon.contains(p):
139+
return False
140+
return True
141+
142+
143+
def generate_all_probes(probe_tables_path, output_folder):
144+
sheet_names = list(pd.read_excel(probe_tables_path / "probe_contacts.xlsx", sheet_name=None).keys())
145+
146+
wrong_contours = []
147+
sheets_with_issues = []
148+
149+
for sheet_name in tqdm(sheet_names, "Exporting CN probes"):
150+
contacts = pd.read_excel(probe_tables_path / "probe_contacts.xlsx", sheet_name=sheet_name)
151+
contour = pd.read_excel(probe_tables_path / "probe_contours.xlsx", sheet_name=sheet_name)
152+
153+
if np.all(pd.isna(contacts["contact_sides"])):
154+
contacts.drop(columns="contact_sides", inplace=True)
155+
156+
if np.all(pd.isna(contacts["shank_ids"])):
157+
contacts.drop(columns="shank_ids", inplace=True)
158+
159+
if "z" in contacts.columns:
160+
contacts.drop(columns=["z"], inplace=True)
161+
try:
162+
probe = Probe.from_dataframe(contacts)
163+
probe.manufacturer = "cambridgeneurotech"
164+
probe.model_name = sheet_name
165+
probe.set_planar_contour(contour)
166+
167+
if not is_contour_correct(probe):
168+
wrong_contours.append(sheet_name)
169+
170+
export_one_probe(sheet_name, probe, output_folder)
171+
172+
except Exception as e:
173+
print(f"Problem loading {sheet_name}: {e}")
174+
sheets_with_issues.append(sheet_name)
175+
176+
print(f"\nREPORT for CambridgeNeurotech probe library generation\n")
177+
print(f"Probes with wrong contours: {len(wrong_contours)}\n{wrong_contours}")
178+
print(f"Probes failed to load: {len(sheets_with_issues)}\n{sheets_with_issues}")
179+
180+
181+
if __name__ == "__main__":
182+
args = parser.parse_args()
183+
probe_tables_path = Path(args.probe_tables_path)
184+
output_folder = Path(args.output_folder)
185+
if output_folder.exists():
186+
shutil.rmtree(output_folder)
187+
output_folder.mkdir(parents=True, exist_ok=True)
188+
generate_all_probes(probe_tables_path, output_folder)

0 commit comments

Comments
 (0)