|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +import pandas as pd |
| 5 | + |
| 6 | +""" |
| 7 | +Geometry definition for Neuropixels probes |
| 8 | +The definition here are all from Jennifer Colonell |
| 9 | +See: |
| 10 | +https://github.com/jenniferColonell/SGLXMetaToCoords/blob/main/SGLXMetaToCoords.py |
| 11 | +
|
| 12 | +A better approach is to pip install and use as a package |
| 13 | +Unfortunately, the GitHub repo above is not yet packaged and pip installable |
| 14 | +
|
| 15 | +Better yet, full integration with ProbeInterface and the probes' geometry |
| 16 | + from Jennifer Colonell - this is in the making! |
| 17 | +
|
| 18 | +Latest update: 07-26-2023 |
| 19 | +""" |
| 20 | + |
| 21 | +# many part numbers have the same geometry parameters ; |
| 22 | +# define those sets in lists |
| 23 | +# [nShank, shankWidth, shankPitch, even_xOff, odd_xOff, horizPitch, vertPitch, rowsPerShank, elecPerShank] |
| 24 | +geom_param_names = [ |
| 25 | + "nShank", |
| 26 | + "shankWidth", |
| 27 | + "shankPitch", |
| 28 | + "even_xOff", |
| 29 | + "odd_xOff", |
| 30 | + "horizPitch", |
| 31 | + "vertPitch", |
| 32 | + "rowsPerShank", |
| 33 | + "elecPerShank", |
| 34 | +] |
| 35 | + |
| 36 | +# offset and pitch values in um |
| 37 | +np1_stag_70um = [1, 70, 0, 27, 11, 32, 20, 480, 960] |
| 38 | +nhp_lin_70um = [1, 70, 0, 27, 27, 32, 20, 480, 960] |
| 39 | +nhp_stag_125um_med = [1, 125, 0, 27, 11, 87, 20, 1368, 2496] |
| 40 | +nhp_stag_125um_long = [1, 125, 0, 27, 11, 87, 20, 2208, 4416] |
| 41 | +nhp_lin_125um_med = [1, 125, 0, 11, 11, 103, 20, 1368, 2496] |
| 42 | +nhp_lin_125um_long = [1, 125, 0, 11, 11, 103, 20, 2208, 4416] |
| 43 | +uhd_8col_1bank = [1, 70, 0, 14, 14, 6, 6, 48, 384] |
| 44 | +uhd_8col_16bank = [1, 70, 0, 14, 14, 6, 6, 768, 6144] |
| 45 | +np2_ss = [1, 70, 0, 27, 27, 32, 15, 640, 1280] |
| 46 | +np2_4s = [4, 70, 250, 27, 27, 32, 15, 640, 1280] |
| 47 | +NP1120 = [1, 70, 0, 6.75, 6.75, 4.5, 4.5, 192, 384] |
| 48 | +NP1121 = [1, 70, 0, 6.25, 6.25, 3, 3, 384, 384] |
| 49 | +NP1122 = [1, 70, 0, 6.75, 6.75, 4.5, 4.5, 24, 384] |
| 50 | +NP1123 = [1, 70, 0, 10.25, 10.25, 3, 3, 32, 384] |
| 51 | +NP1300 = [1, 70, 0, 11, 11, 48, 20, 480, 960] |
| 52 | +NP1200 = [1, 70, 0, 27, 11, 32, 20, 64, 128] |
| 53 | +NXT3000 = [1, 70, 0, 53, 53, 0, 15, 128, 128] |
| 54 | + |
| 55 | +""" |
| 56 | +Electrode coordinate system - from Bill Karsh |
| 57 | +(https://github.com/billkarsh/SpikeGLX/blob/master/Markdown/Metadata_30.md) |
| 58 | +
|
| 59 | +The X-origin is the left edge of the shank |
| 60 | +The Y-origin is the center of the bottom-most elecrode row (closest to the tip) |
| 61 | +""" |
| 62 | + |
| 63 | + |
| 64 | +M = dict( |
| 65 | + [ |
| 66 | + ("3A", np1_stag_70um), |
| 67 | + ("PRB_1_4_0480_1", np1_stag_70um), |
| 68 | + ("PRB_1_4_0480_1_C", np1_stag_70um), |
| 69 | + ("NP1010", np1_stag_70um), |
| 70 | + ("NP1011", np1_stag_70um), |
| 71 | + ("NP1012", np1_stag_70um), |
| 72 | + ("NP1013", np1_stag_70um), |
| 73 | + ("NP1015", nhp_lin_70um), |
| 74 | + ("NP1016", nhp_lin_70um), |
| 75 | + ("NP1017", nhp_lin_70um), |
| 76 | + ("NP1020", nhp_stag_125um_med), |
| 77 | + ("NP1021", nhp_stag_125um_med), |
| 78 | + ("NP1030", nhp_stag_125um_long), |
| 79 | + ("NP1031", nhp_stag_125um_long), |
| 80 | + ("NP1022", nhp_lin_125um_med), |
| 81 | + ("NP1032", nhp_lin_125um_long), |
| 82 | + ("NP1100", uhd_8col_1bank), |
| 83 | + ("NP1110", uhd_8col_16bank), |
| 84 | + ("PRB2_1_4_0480_1", np2_ss), |
| 85 | + ("PRB2_1_2_0640_0", np2_ss), |
| 86 | + ("NP2000", np2_ss), |
| 87 | + ("NP2003", np2_ss), |
| 88 | + ("NP2004", np2_ss), |
| 89 | + ("PRB2_4_2_0640_0", np2_4s), |
| 90 | + ("PRB2_4_4_0480_1", np2_4s), |
| 91 | + ("NP2010", np2_4s), |
| 92 | + ("NP2013", np2_4s), |
| 93 | + ("NP2014", np2_4s), |
| 94 | + ("NP1120", NP1120), |
| 95 | + ("NP1121", NP1121), |
| 96 | + ("NP1122", NP1122), |
| 97 | + ("NP1123", NP1123), |
| 98 | + ("NP1300", NP1300), |
| 99 | + ("NP1200", NP1200), |
| 100 | + ("NXT3000", NXT3000), |
| 101 | + ] |
| 102 | +) |
| 103 | + |
| 104 | + |
| 105 | +def build_npx_probe( |
| 106 | + nShank: int, |
| 107 | + shankWidth: float, |
| 108 | + shankPitch: float, |
| 109 | + even_xOff: float, |
| 110 | + odd_xOff: float, |
| 111 | + horizPitch: float, |
| 112 | + vertPitch: float, |
| 113 | + rowsPerShank: int, |
| 114 | + elecPerShank: int, |
| 115 | + probe_type: str = "neuropixels", |
| 116 | +): |
| 117 | + row_offset = np.tile([even_xOff, odd_xOff], int(rowsPerShank / 2)) |
| 118 | + |
| 119 | + elec_pos_df = build_electrode_layouts( |
| 120 | + probe_type=probe_type, |
| 121 | + site_count_per_shank=elecPerShank, |
| 122 | + col_spacing=horizPitch, |
| 123 | + row_spacing=vertPitch, |
| 124 | + row_offset=row_offset, |
| 125 | + col_count_per_shank=int(elecPerShank / rowsPerShank), |
| 126 | + shank_count=nShank, |
| 127 | + shank_spacing=shankPitch, |
| 128 | + y_origin="bottom", |
| 129 | + as_dataframe=True, |
| 130 | + ) |
| 131 | + |
| 132 | + return elec_pos_df |
| 133 | + |
| 134 | + |
| 135 | +def to_probeinterface(electrodes_df): |
| 136 | + from probeinterface import Probe |
| 137 | + |
| 138 | + probe_df = electrodes_df.copy() |
| 139 | + probe_df.rename( |
| 140 | + columns={ |
| 141 | + "electrode": "contact_ids", |
| 142 | + "shank": "shank_ids", |
| 143 | + "x_coord": "x", |
| 144 | + "y_coord": "y", |
| 145 | + }, |
| 146 | + inplace=True, |
| 147 | + ) |
| 148 | + probe_df["contact_shapes"] = "square" |
| 149 | + probe_df["width"] = 12 |
| 150 | + |
| 151 | + return Probe.from_dataframe(probe_df) |
| 152 | + |
| 153 | + |
| 154 | +def build_electrode_layouts( |
| 155 | + probe_type: str, |
| 156 | + site_count_per_shank: int, |
| 157 | + col_spacing: float = None, |
| 158 | + row_spacing: float = None, |
| 159 | + row_offset: list = None, |
| 160 | + col_count_per_shank: int = 1, |
| 161 | + shank_count: int = 1, |
| 162 | + shank_spacing: float = None, |
| 163 | + y_origin="bottom", |
| 164 | + as_dataframe=False, |
| 165 | +) -> list[dict]: |
| 166 | + """Builds electrode layouts. |
| 167 | +
|
| 168 | + Args: |
| 169 | + probe_type (str): probe type (e.g., "neuropixels 1.0 - 3A"). |
| 170 | + site_count_per_shank (int): site count per shank. |
| 171 | + col_spacing (float): (um) horizontal spacing between sites. Defaults to None (single column). |
| 172 | + row_spacing (float): (um) vertical spacing between columns. Defaults to None (single row). |
| 173 | + row_offset (list): (um) per-row offset spacing. Defaults to None. |
| 174 | + col_count_per_shank (int): number of column per shank. Defaults to 1 (single column). |
| 175 | + shank_count (int): number of shank. Defaults to 1 (single shank). |
| 176 | + shank_spacing (float): (um) spacing between shanks. Defaults to None (single shank). |
| 177 | + y_origin (str): {"bottom", "top"}. y value decrements if "top". Defaults to "bottom". |
| 178 | + as_dataframe (bool): if True, returns as pandas DataFrame, otherwise as list of dict |
| 179 | + """ |
| 180 | + row_count = int(site_count_per_shank / col_count_per_shank) |
| 181 | + x_coords = np.tile( |
| 182 | + np.arange(0, (col_spacing or 1) * col_count_per_shank, (col_spacing or 1)), |
| 183 | + row_count, |
| 184 | + ) |
| 185 | + y_coords = np.repeat(np.arange(row_count) * (row_spacing or 1), col_count_per_shank) |
| 186 | + |
| 187 | + if row_offset is None: |
| 188 | + row_offset = np.zeros_like(x_coords) |
| 189 | + else: |
| 190 | + assert len(row_offset) == row_count |
| 191 | + row_offset = np.tile(row_offset, col_count_per_shank) |
| 192 | + x_coords = x_coords + row_offset |
| 193 | + |
| 194 | + shank_cols = np.tile(range(col_count_per_shank), row_count) |
| 195 | + shank_rows = np.repeat(range(row_count), col_count_per_shank) |
| 196 | + |
| 197 | + electrode_layout = [ |
| 198 | + { |
| 199 | + "probe_type": probe_type, |
| 200 | + "electrode": (site_count_per_shank * shank_no) + e_id, |
| 201 | + "shank": shank_no, |
| 202 | + "shank_col": c_id, |
| 203 | + "shank_row": r_id, |
| 204 | + "x_coord": x + (shank_no * (shank_spacing or 1)), |
| 205 | + "y_coord": {"top": -y, "bottom": y}[y_origin], |
| 206 | + } |
| 207 | + for shank_no in range(shank_count) |
| 208 | + for e_id, (c_id, r_id, x, y) in enumerate( |
| 209 | + zip(shank_cols, shank_rows, x_coords, y_coords) |
| 210 | + ) |
| 211 | + ] |
| 212 | + |
| 213 | + return pd.DataFrame(electrode_layout) if as_dataframe else electrode_layout |
0 commit comments