|
1 | 1 | """Generator for Diagnostic Biochips Deep Array probes. |
2 | 2 |
|
3 | | -Builds the Deep Array models from the 2022 Diagnostic Biochips catalog |
4 | | -datasheets and writes each as diagnosticbiochips/<model>/<model>.json in the |
5 | | -probeinterface format. |
6 | | -
|
7 | | -Two layout families are produced: |
8 | | -
|
9 | | -- Linear models (DA128-1, DA64-1, DA32-1, DA32-2): a single column of sites, |
10 | | - pitch from the datasheet, span = (n_contacts - 1) * pitch. |
11 | | -
|
12 | | -- Staggered models (DA128-2, DA64-2): a two-column triangular (hexagonal) |
13 | | - lattice. The columns are 43.3 um apart horizontally, sites alternate columns |
14 | | - going down the shank with a 25 um vertical step (so 50 um within a column), |
15 | | - and the diagonal nearest-neighbor distance is sqrt(43.3**2 + 25**2) = 50 um. |
16 | | - The datasheet schematic is "not drawn to scale", but span = (n_contacts - 1) |
17 | | - * 25 um matches the printed recording span exactly for both models, which |
18 | | - fixes the layout to one site per 25 um level, i.e. exactly two columns. |
| 3 | +The source of truth is the 2022 Diagnostic Biochips catalog (one datasheet page |
| 4 | +per model). Each model's spec table and schematic dimensions are transcribed |
| 5 | +verbatim below into a DeepArraySpec, in the exact units printed on the sheet. |
| 6 | +Everything written to diagnosticbiochips/<model>/<model>.json is derived from |
| 7 | +those values; nothing geometric is invented here. |
| 8 | +
|
| 9 | +The probeinterface JSON follows the same convention as the Neuropixels and |
| 10 | +Cambridge NeuroTech entries in this library: the geometry is stored |
| 11 | +geometrically (contact positions, contact shape, full-shank contour) and the |
| 12 | +annotations carry only what the geometry cannot express. For these passive |
| 13 | +probes that is just the shank material; site diameter, shank diameter, max |
| 14 | +shank length and recording span are all recoverable from the geometry. |
| 15 | +
|
| 16 | +Two layout families appear on the datasheets: |
| 17 | +
|
| 18 | +- Linear (DA128-1, DA64-1, DA32-1, DA32-2): a single column of sites at the |
| 19 | + printed vertical pitch. |
| 20 | +
|
| 21 | +- Staggered (DA128-2, DA64-2): a two-column triangular (hexagonal) lattice. |
| 22 | + The columns are 43.3 um apart horizontally and sites alternate columns going |
| 23 | + up the shank with a 25 um vertical step (so 50 um within a column); the |
| 24 | + diagonal nearest-neighbor distance is sqrt(43.3**2 + 25**2) = 50 um. The |
| 25 | + schematic is "not drawn to scale", but (n_channels - 1) * 25 um equals the |
| 26 | + printed recording span exactly, which fixes the layout to one site per 25 um |
| 27 | + level, i.e. exactly two columns. This is checked against the datasheet value |
| 28 | + for every model before writing (see _build_probe). |
19 | 29 |
|
20 | 30 | Run with: uv run --with probeinterface scripts/generate_diagnostic_biochips.py |
21 | 31 | """ |
22 | 32 |
|
| 33 | +from dataclasses import dataclass |
23 | 34 | from pathlib import Path |
24 | 35 |
|
25 | 36 | from probeinterface import Probe, ProbeGroup, write_probeinterface |
26 | 37 |
|
27 | 38 | MANUFACTURER = "diagnosticbiochips" |
28 | | -SITE_DIAMETER_UM = 20.0 |
29 | | -SHANK_WIDTH_UM = 200.0 # 0.2 mm stainless steel shank |
30 | | -TIP_LENGTH_UM = 150.0 # cosmetic pointed tip below the deepest site |
31 | | - |
32 | | -# (model, n_contacts, vertical_pitch_um) |
33 | | -LINEAR_MODELS = [ |
34 | | - ("DA128-1", 128, 40.0), |
35 | | - ("DA64-1", 64, 100.0), |
36 | | - ("DA32-1", 32, 65.0), |
37 | | - ("DA32-2", 32, 100.0), |
38 | | -] |
39 | | - |
40 | | -# (model, n_contacts) -- shared staggered lattice constants below. |
41 | | -STAGGERED_MODELS = [ |
42 | | - ("DA128-2", 128), |
43 | | - ("DA64-2", 64), |
| 39 | +TIP_LENGTH_UM = 150.0 # cosmetic pointed tip below the shank base (not on the sheet) |
| 40 | + |
| 41 | + |
| 42 | +@dataclass(frozen=True) |
| 43 | +class DeepArraySpec: |
| 44 | + """Verbatim transcription of one 2022 Diagnostic Biochips catalog page. |
| 45 | +
|
| 46 | + Spec-table fields keep the units printed on the datasheet; the two layout |
| 47 | + fields are read from the schematic dimension callouts. |
| 48 | + """ |
| 49 | + |
| 50 | + model: str |
| 51 | + num_channels: int |
| 52 | + layout: str # "linear" or "staggered" |
| 53 | + # Spec table |
| 54 | + site_diameter_um: float |
| 55 | + shank_diameter_mm: float |
| 56 | + max_shank_length_mm: float |
| 57 | + recording_span_mm: float |
| 58 | + shank_material: str |
| 59 | + # Schematic dimension callouts |
| 60 | + vertical_pitch_um: float # linear: column pitch; staggered: step between alternating sites |
| 61 | + horizontal_offset_um: float | None = None # staggered only: full column separation |
| 62 | + |
| 63 | + |
| 64 | +# One entry per datasheet page, values exactly as printed on the 2022 catalog. |
| 65 | +DATASHEETS = [ |
| 66 | + DeepArraySpec("DA128-1", 128, "linear", 20.0, 0.2, 90.0, 5.08, "stainless steel", 40.0), |
| 67 | + DeepArraySpec("DA64-1", 64, "linear", 20.0, 0.2, 90.0, 6.3, "stainless steel", 100.0), |
| 68 | + DeepArraySpec("DA32-1", 32, "linear", 20.0, 0.2, 90.0, 2.015, "stainless steel", 65.0), |
| 69 | + DeepArraySpec("DA32-2", 32, "linear", 20.0, 0.2, 90.0, 3.1, "stainless steel", 100.0), |
| 70 | + DeepArraySpec("DA128-2", 128, "staggered", 20.0, 0.2, 90.0, 3.175, "stainless steel", 25.0, 43.3), |
| 71 | + DeepArraySpec("DA64-2", 64, "staggered", 20.0, 0.2, 90.0, 1.575, "stainless steel", 25.0, 43.3), |
44 | 72 | ] |
45 | | -STAGGERED_X_OFFSET_UM = 21.65 # 43.3 um column separation -> +/-21.65 |
46 | | -STAGGERED_VERTICAL_STEP_UM = 25.0 # step between alternating columns |
47 | 73 |
|
48 | 74 |
|
49 | | -def _shank_contour(span_um: float) -> list: |
50 | | - half_width = SHANK_WIDTH_UM / 2.0 |
51 | | - y_top = span_um + half_width |
| 75 | +def _contact_positions(spec: DeepArraySpec) -> list: |
| 76 | + # Site 0 sits at the tip (y = 0) and the band grows up the shank. |
| 77 | + if spec.layout == "linear": |
| 78 | + # Single column centered on the shank. |
| 79 | + return [[0.0, index * spec.vertical_pitch_um] for index in range(spec.num_channels)] |
| 80 | + if spec.layout == "staggered": |
| 81 | + # Two columns +/-(horizontal_offset / 2), alternating every site. |
| 82 | + half_offset = spec.horizontal_offset_um / 2.0 |
| 83 | + positions = [] |
| 84 | + for index in range(spec.num_channels): |
| 85 | + x = -half_offset if index % 2 == 0 else half_offset |
| 86 | + positions.append([x, index * spec.vertical_pitch_um]) |
| 87 | + return positions |
| 88 | + raise ValueError(f"unknown layout {spec.layout!r} for {spec.model}") |
| 89 | + |
| 90 | + |
| 91 | +def _shank_contour(spec: DeepArraySpec) -> list: |
| 92 | + # Outline of the full physical shaft (tip to body): the printed shank |
| 93 | + # diameter sets the width, the printed max shank length sets the height. |
| 94 | + half_width = spec.shank_diameter_mm * 1000.0 / 2.0 |
| 95 | + length_um = spec.max_shank_length_mm * 1000.0 |
52 | 96 | return [ |
53 | | - [-half_width, y_top], |
| 97 | + [-half_width, length_um], |
54 | 98 | [-half_width, -half_width], |
55 | 99 | [0.0, -half_width - TIP_LENGTH_UM], |
56 | 100 | [half_width, -half_width], |
57 | | - [half_width, y_top], |
| 101 | + [half_width, length_um], |
58 | 102 | ] |
59 | 103 |
|
60 | 104 |
|
61 | | -def build_linear_probe(model_name: str, n_contacts: int, pitch_um: float) -> Probe: |
62 | | - # Single column centered on the shank; site 0 at the tip, y increasing up. |
63 | | - positions = [[0.0, index * pitch_um] for index in range(n_contacts)] |
| 105 | +def _build_probe(spec: DeepArraySpec) -> Probe: |
| 106 | + positions = _contact_positions(spec) |
64 | 107 |
|
65 | | - probe = Probe(ndim=2, si_units="um") |
66 | | - probe.set_contacts( |
67 | | - positions=positions, |
68 | | - shapes="circle", |
69 | | - shape_params={"radius": SITE_DIAMETER_UM / 2.0}, |
| 108 | + # The contact extent must reproduce the printed recording span. |
| 109 | + span_um = positions[-1][1] - positions[0][1] |
| 110 | + expected_um = spec.recording_span_mm * 1000.0 |
| 111 | + assert abs(span_um - expected_um) < 1e-6, ( |
| 112 | + f"{spec.model}: derived span {span_um} um != datasheet {expected_um} um" |
70 | 113 | ) |
71 | | - probe.annotate(model_name=model_name, manufacturer=MANUFACTURER) |
72 | | - probe.set_contact_ids([str(index) for index in range(n_contacts)]) |
73 | | - probe.set_planar_contour(_shank_contour((n_contacts - 1) * pitch_um)) |
74 | | - return probe |
75 | | - |
76 | | - |
77 | | -def build_staggered_probe(model_name: str, n_contacts: int) -> Probe: |
78 | | - # Two columns +/-STAGGERED_X_OFFSET_UM apart; sites alternate columns going |
79 | | - # up the shank with a STAGGERED_VERTICAL_STEP_UM step (site 0 at the tip). |
80 | | - positions = [] |
81 | | - for index in range(n_contacts): |
82 | | - x = -STAGGERED_X_OFFSET_UM if index % 2 == 0 else STAGGERED_X_OFFSET_UM |
83 | | - positions.append([x, index * STAGGERED_VERTICAL_STEP_UM]) |
84 | 114 |
|
85 | 115 | probe = Probe(ndim=2, si_units="um") |
86 | 116 | probe.set_contacts( |
87 | 117 | positions=positions, |
88 | 118 | shapes="circle", |
89 | | - shape_params={"radius": SITE_DIAMETER_UM / 2.0}, |
| 119 | + shape_params={"radius": spec.site_diameter_um / 2.0}, |
90 | 120 | ) |
91 | | - probe.annotate(model_name=model_name, manufacturer=MANUFACTURER) |
92 | | - probe.set_contact_ids([str(index) for index in range(n_contacts)]) |
93 | | - probe.set_planar_contour(_shank_contour((n_contacts - 1) * STAGGERED_VERTICAL_STEP_UM)) |
| 121 | + # Geometry encodes site diameter (radius), shank diameter (contour width), |
| 122 | + # max shank length (contour height) and recording span (contact extent), so |
| 123 | + # shank material is the only spec stored as an annotation. |
| 124 | + probe.annotate( |
| 125 | + model_name=spec.model, |
| 126 | + manufacturer=MANUFACTURER, |
| 127 | + shank_material=spec.shank_material, |
| 128 | + ) |
| 129 | + probe.set_contact_ids([str(index) for index in range(spec.num_channels)]) |
| 130 | + probe.set_planar_contour(_shank_contour(spec)) |
94 | 131 | return probe |
95 | 132 |
|
96 | 133 |
|
97 | | -def _write(root: Path, probe: Probe, model_name: str, n_contacts: int, span_um: float) -> None: |
98 | | - group = ProbeGroup() |
99 | | - group.add_probe(probe) |
100 | | - out_dir = root / MANUFACTURER / model_name |
101 | | - out_dir.mkdir(parents=True, exist_ok=True) |
102 | | - out_file = out_dir / f"{model_name}.json" |
103 | | - write_probeinterface(out_file, group) |
104 | | - print(f"wrote {out_file} ({n_contacts}ch, {span_um / 1000.0:g} mm span)") |
105 | | - |
106 | | - |
107 | 134 | def main() -> None: |
108 | 135 | root = Path(__file__).resolve().parent.parent |
109 | | - for model_name, n_contacts, pitch_um in LINEAR_MODELS: |
110 | | - probe = build_linear_probe(model_name, n_contacts, pitch_um) |
111 | | - _write(root, probe, model_name, n_contacts, (n_contacts - 1) * pitch_um) |
112 | | - for model_name, n_contacts in STAGGERED_MODELS: |
113 | | - probe = build_staggered_probe(model_name, n_contacts) |
114 | | - span_um = (n_contacts - 1) * STAGGERED_VERTICAL_STEP_UM |
115 | | - _write(root, probe, model_name, n_contacts, span_um) |
| 136 | + for spec in DATASHEETS: |
| 137 | + probe = _build_probe(spec) |
| 138 | + group = ProbeGroup() |
| 139 | + group.add_probe(probe) |
| 140 | + |
| 141 | + out_dir = root / MANUFACTURER / spec.model |
| 142 | + out_dir.mkdir(parents=True, exist_ok=True) |
| 143 | + out_file = out_dir / f"{spec.model}.json" |
| 144 | + write_probeinterface(out_file, group) |
| 145 | + print(f"wrote {out_file} ({spec.num_channels}ch, {spec.layout}, {spec.recording_span_mm:g} mm span)") |
116 | 146 |
|
117 | 147 |
|
118 | 148 | if __name__ == "__main__": |
|
0 commit comments