From 6ff5fa8dde2cea66e3fa9bd4df90e605ed0fbb37 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 10 Jun 2026 11:36:08 -0600 Subject: [PATCH 1/2] User friendly manual for cambridge probes --- .../src/grouping/cambridgeneurotech.json | 585 ++++++++++++++++++ apps/probe-viewer/src/grouping/index.ts | 2 + scripts/generate_cambridge_grouping.py | 142 +++++ 3 files changed, 729 insertions(+) create mode 100644 apps/probe-viewer/src/grouping/cambridgeneurotech.json create mode 100644 scripts/generate_cambridge_grouping.py diff --git a/apps/probe-viewer/src/grouping/cambridgeneurotech.json b/apps/probe-viewer/src/grouping/cambridgeneurotech.json new file mode 100644 index 0000000..f7d9e14 --- /dev/null +++ b/apps/probe-viewer/src/grouping/cambridgeneurotech.json @@ -0,0 +1,585 @@ +{ + "hierarchy": [ + { + "label": "H (high-resolution / dense recording)", + "children": [ + { + "label": "32 channel", + "children": [ + { + "label": "H1b (1 shank)", + "probes": [ + "ASSY-37-H1b", + "ASSY-116-H1b" + ] + }, + { + "label": "H4 (1 shank)", + "probes": [ + "ASSY-37-H4", + "ASSY-116-H4", + "ASSY-196-H4" + ] + }, + { + "label": "H6b (1 shank)", + "probes": [ + "ASSY-37-H6b", + "ASSY-116-H6b", + "ASSY-196-H6b" + ] + }, + { + "label": "H7b (1 shank)", + "probes": [ + "ASSY-37-H7b", + "ASSY-116-H7b" + ] + }, + { + "label": "H8b (1 shank)", + "probes": [ + "ASSY-37-H8b", + "ASSY-116-H8b" + ] + }, + { + "label": "H10b (1 shank)", + "probes": [ + "ASSY-37-H10b", + "ASSY-116-H10b", + "ASSY-196-H10b" + ] + } + ] + }, + { + "label": "64 channel", + "children": [ + { + "label": "H1 (2 shanks)", + "probes": [ + "ASSY-77-H1", + "ASSY-156-H1", + "ASSY-236-H1" + ] + }, + { + "label": "H2 (2 shanks)", + "probes": [ + "ASSY-77-H2", + "ASSY-156-H2", + "ASSY-158-H2", + "ASSY-236-H2", + "ASSY-276-H2", + "ASSY-325-H2" + ] + }, + { + "label": "H3 (1 shank)", + "probes": [ + "ASSY-37-H3", + "ASSY-77-H3", + "ASSY-156-H3", + "ASSY-158-H3", + "ASSY-236-H3", + "ASSY-276-H3", + "ASSY-325-H3" + ] + }, + { + "label": "H5 (1 shank)", + "probes": [ + "ASSY-77-H5", + "ASSY-156-H5", + "ASSY-158-H5", + "ASSY-236-H5", + "ASSY-276-H5", + "ASSY-325-H5" + ] + }, + { + "label": "H6 (2 shanks)", + "probes": [ + "ASSY-77-H6", + "ASSY-156-H6", + "ASSY-158-H6", + "ASSY-236-H6", + "ASSY-276-H6", + "ASSY-325-H6" + ] + }, + { + "label": "H7 (2 shanks)", + "probes": [ + "ASSY-77-H7", + "ASSY-156-H7", + "ASSY-158-H7", + "ASSY-236-H7", + "ASSY-276-H7", + "ASSY-325-H7" + ] + }, + { + "label": "H8 (2 shanks)", + "probes": [ + "ASSY-77-H8", + "ASSY-156-H8", + "ASSY-158-H8", + "ASSY-236-H8", + "ASSY-276-H8", + "ASSY-325-H8" + ] + }, + { + "label": "H9 (1 shank)", + "probes": [ + "ASSY-77-H9", + "ASSY-156-H9", + "ASSY-158-H9", + "ASSY-236-H9", + "ASSY-276-H9", + "ASSY-325-H9" + ] + }, + { + "label": "H10 (2 shanks)", + "probes": [ + "ASSY-77-H10", + "ASSY-156-H10", + "ASSY-158-H10", + "ASSY-236-H10", + "ASSY-276-H10", + "ASSY-325-H10" + ] + } + ] + }, + { + "label": "128 channel", + "children": [ + { + "label": "H7 (1 shank)", + "probes": [ + "ASSY-325D-H7" + ] + }, + { + "label": "H10 (1 shank)", + "probes": [ + "ASSY-325D-H10" + ] + }, + { + "label": "H12 (1 shank)", + "probes": [ + "ASSY-350-H12" + ] + }, + { + "label": "H13 (1 shank)", + "probes": [ + "ASSY-350-H13" + ] + }, + { + "label": "H14-1 (4 shanks)", + "probes": [ + "ASSY-350-H14-1" + ] + }, + { + "label": "H14-2 (4 shanks)", + "probes": [ + "ASSY-350-H14-2" + ] + }, + { + "label": "H15 (2 shanks)", + "probes": [ + "ASSY-350-H15" + ] + }, + { + "label": "H15_2 (2 shanks)", + "probes": [ + "ASSY-350-H15_2" + ] + }, + { + "label": "H16 (4 shanks)", + "probes": [ + "ASSY-350-H16" + ] + }, + { + "label": "H20 (4 shanks)", + "probes": [ + "ASSY-350-H20" + ] + } + ] + } + ] + }, + { + "label": "P (compact array)", + "children": [ + { + "label": "16 channel", + "children": [ + { + "label": "P-1 (1 shank)", + "probes": [ + "ASSY-1-P-1", + "ASSY-79-P-1" + ] + }, + { + "label": "P-2 (1 shank)", + "probes": [ + "ASSY-1-P-2", + "ASSY-79-P-2" + ] + } + ] + }, + { + "label": "32 channel", + "children": [ + { + "label": "P-1 (2 shanks)", + "probes": [ + "ASSY-37-P-1", + "ASSY-116-P-1", + "ASSY-196-P-1" + ] + }, + { + "label": "P-2 (2 shanks)", + "probes": [ + "ASSY-37-P-2", + "ASSY-116-P-2", + "ASSY-196-P-2" + ] + } + ] + }, + { + "label": "64 channel", + "children": [ + { + "label": "P-1 (4 shanks)", + "probes": [ + "ASSY-77-P-1", + "ASSY-156-P-1", + "ASSY-158-P-1", + "ASSY-236-P-1", + "ASSY-276-P-1", + "ASSY-325-P-1" + ] + }, + { + "label": "P-2 (4 shanks)", + "probes": [ + "ASSY-77-P-2", + "ASSY-156-P-2", + "ASSY-158-P-2", + "ASSY-236-P-2", + "ASSY-276-P-2", + "ASSY-325-P-2" + ] + } + ] + }, + { + "label": "128 channel", + "children": [ + { + "label": "P-1 (1 shank)", + "probes": [ + "ASSY-325D-P-1" + ] + }, + { + "label": "P-2 (1 shank)", + "probes": [ + "ASSY-325D-P-2" + ] + } + ] + } + ], + "note": "Descriptive label inferred from geometry; Cambridge publishes an official name only for the H-series." + }, + { + "label": "E (compact edge array)", + "children": [ + { + "label": "16 channel", + "children": [ + { + "label": "E-1 (1 shank)", + "probes": [ + "ASSY-1-E-1", + "ASSY-79-E-1" + ] + }, + { + "label": "E-2 (1 shank)", + "probes": [ + "ASSY-1-E-2", + "ASSY-79-E-2" + ] + } + ] + }, + { + "label": "32 channel", + "children": [ + { + "label": "E-1 (2 shanks)", + "probes": [ + "ASSY-37-E-1", + "ASSY-116-E-1", + "ASSY-196-E-1" + ] + }, + { + "label": "E-2 (2 shanks)", + "probes": [ + "ASSY-37-E-2", + "ASSY-116-E-2", + "ASSY-196-E-2" + ] + } + ] + }, + { + "label": "64 channel", + "children": [ + { + "label": "E-1 (4 shanks)", + "probes": [ + "ASSY-77-E-1", + "ASSY-156-E-1", + "ASSY-158-E-1", + "ASSY-236-E-1", + "ASSY-276-E-1", + "ASSY-325-E-1" + ] + }, + { + "label": "E-2 (4 shanks)", + "probes": [ + "ASSY-77-E-2", + "ASSY-156-E-2", + "ASSY-158-E-2", + "ASSY-236-E-2", + "ASSY-276-E-2", + "ASSY-325-E-2" + ] + } + ] + }, + { + "label": "128 channel", + "children": [ + { + "label": "E-1 (4 shanks)", + "probes": [ + "ASSY-325D-E-1" + ] + }, + { + "label": "E-2 (4 shanks)", + "probes": [ + "ASSY-325D-E-2" + ] + } + ] + } + ], + "note": "Descriptive label inferred from geometry; Cambridge publishes an official name only for the H-series." + }, + { + "label": "F (fine-pitch multi-shank)", + "children": [ + { + "label": "32 channel", + "children": [ + { + "label": "Fb (3 shanks)", + "probes": [ + "ASSY-37-Fb", + "ASSY-116-Fb", + "ASSY-196-Fb" + ] + } + ] + }, + { + "label": "64 channel", + "children": [ + { + "label": "F (6 shanks)", + "probes": [ + "ASSY-77-F", + "ASSY-156-F", + "ASSY-158-F", + "ASSY-236-F", + "ASSY-276-F", + "ASSY-325-F" + ] + } + ] + }, + { + "label": "128 channel", + "children": [ + { + "label": "F (6 shanks)", + "probes": [ + "ASSY-325D-F" + ] + }, + { + "label": "F8-0 (8 shanks)", + "probes": [ + "ASSY-350-F8-0" + ] + }, + { + "label": "F8-1 (8 shanks)", + "probes": [ + "ASSY-350-F8-1" + ] + }, + { + "label": "F8-2 (8 shanks)", + "probes": [ + "ASSY-350-F8-2" + ] + } + ] + } + ], + "note": "Descriptive label inferred from geometry; Cambridge publishes an official name only for the H-series." + }, + { + "label": "L (linear / laminar)", + "children": [ + { + "label": "16 channel", + "children": [ + { + "label": "L1 (1 shank)", + "probes": [ + "ASSY-79-L1" + ] + } + ] + }, + { + "label": "32 channel", + "children": [ + { + "label": "L2 (2 shanks)", + "probes": [ + "ASSY-37-L2", + "ASSY-116-L2" + ] + } + ] + }, + { + "label": "64 channel", + "children": [ + { + "label": "L3 (1 shank)", + "probes": [ + "ASSY-77-L3", + "ASSY-156-L3", + "ASSY-158-L3", + "ASSY-236-L3", + "ASSY-276-L3", + "ASSY-325-L3" + ] + } + ] + }, + { + "label": "128 channel", + "children": [ + { + "label": "L13 (1 shank)", + "probes": [ + "ASSY-350-L13" + ] + }, + { + "label": "L14 (8 shanks)", + "probes": [ + "ASSY-350-L14" + ] + } + ] + } + ], + "note": "Descriptive label inferred from geometry; Cambridge publishes an official name only for the H-series." + }, + { + "label": "M (single-shank)", + "children": [ + { + "label": "64 channel", + "children": [ + { + "label": "M1v1 (1 shank)", + "probes": [ + "ASSY-77-M1v1", + "ASSY-156-M1v1", + "ASSY-158-M1v1", + "ASSY-236-M1v1", + "ASSY-276-M1v1" + ] + }, + { + "label": "M1v2 (1 shank)", + "probes": [ + "ASSY-77-M1v2", + "ASSY-156-M1v2", + "ASSY-158-M1v2", + "ASSY-236-M1v2", + "ASSY-276-M1v2", + "ASSY-325-M1v2" + ] + }, + { + "label": "M2v1 (1 shank)", + "probes": [ + "ASSY-77-M2v1", + "ASSY-156-M2v1", + "ASSY-158-M2v1", + "ASSY-236-M2v1", + "ASSY-276-M2v1" + ] + }, + { + "label": "M2v2 (1 shank)", + "probes": [ + "ASSY-77-M2v2", + "ASSY-156-M2v2", + "ASSY-158-M2v2", + "ASSY-236-M2v2", + "ASSY-276-M2v2", + "ASSY-325-M2v2" + ] + } + ] + } + ], + "note": "Descriptive label inferred from geometry; Cambridge publishes an official name only for the H-series." + } + ] +} diff --git a/apps/probe-viewer/src/grouping/index.ts b/apps/probe-viewer/src/grouping/index.ts index 28b110d..23a0152 100644 --- a/apps/probe-viewer/src/grouping/index.ts +++ b/apps/probe-viewer/src/grouping/index.ts @@ -1,5 +1,6 @@ import type { HierarchyConfig } from "./types"; import imecHierarchy from "./imec_neuropixels.json"; +import cambridgeNeurotechHierarchy from "./cambridgeneurotech.json"; // Registry mapping a manufacturer key (as it appears in the manifest) to its // explicit sidebar hierarchy. A manufacturer absent here has no hierarchy and @@ -10,6 +11,7 @@ import imecHierarchy from "./imec_neuropixels.json"; // JSON is used because Vite imports it with no extra dependency. const REGISTRY: Record = { imec: imecHierarchy as HierarchyConfig, + cambridgeneurotech: cambridgeNeurotechHierarchy as HierarchyConfig, }; export function getGroupingConfig(manufacturer: string): HierarchyConfig | undefined { diff --git a/scripts/generate_cambridge_grouping.py b/scripts/generate_cambridge_grouping.py new file mode 100644 index 0000000..f7287b2 --- /dev/null +++ b/scripts/generate_cambridge_grouping.py @@ -0,0 +1,142 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [] +# /// +"""Generate the explicit sidebar-grouping config for Cambridge NeuroTech. + +The probe-viewer sidebar groups one manufacturer from a hand-curated JSON tree +(see apps/probe-viewer/src/grouping/). This script emits that JSON for Cambridge +NeuroTech so every one of its ~171 models is placed (no "Ungrouped" bucket), +then it can be hand-tuned. It reads only the committed probe JSON (stdlib only). + +Decode of the model id ASSY--: + - series number -> channel count + acute/chronic + connector (SERIES table) + - variant letter -> electrode-layout family (E/P/F/H/L/M) + - full variant -> the specific geometry (H3, P-1, M1v1, F8-0, ...) + +Hierarchy (chosen after web research on how these probes are cited): the type +letter is the top level (researchers cite the geometry, e.g. "H3", and Cambridge +brands the H family), then channel count, then the specific geometry variant as +the leaf, whose probes are the packaging options (the different ASSY numbers). + +Run with: uv run scripts/generate_cambridge_grouping.py +""" + +import json +import re +from pathlib import Path + +MANUFACTURER = "cambridgeneurotech" + +# series number -> (modality, channels, connector). From the Cambridge NeuroTech +# probe-maps table (cambridgeneurotech.com/probe-maps); used for sort order only. +SERIES = { + "1": ("Acute", 16, "Samtec"), + "37": ("Acute", 32, "Samtec"), + "77": ("Acute", 64, "Samtec"), + "79": ("Chronic", 16, "Omnetics"), + "116": ("Chronic", 32, "Omnetics"), + "156": ("Chronic", 64, "Omnetics"), + "158": ("Chronic", 64, "Omnetics"), + "196": ("Chronic", 32, "Molex"), + "236": ("Chronic", 64, "Molex"), + "276": ("Chronic", 64, "TDT Zif-Clip"), + "325": ("Chronic", 64, "Intan (digital)"), + "325D": ("Chronic", 128, "Intan (digital)"), + "350": ("Chronic", 128, "Intan (digital)"), +} + +# Type-letter families, in display order. H first (largest, the only family +# Cambridge brands). The parenthetical for E/P/F/L/M is a descriptive label +# inferred from geometry, not an official Cambridge name (only "H-series" is +# documented); recorded in TYPE_NOTE below. +TYPE_ORDER = ["H", "P", "E", "F", "L", "M"] +TYPE_LABEL = { + "H": "H (high-resolution / dense recording)", + "P": "P (compact array)", + "E": "E (compact edge array)", + "F": "F (fine-pitch multi-shank)", + "L": "L (linear / laminar)", + "M": "M (single-shank)", +} +TYPE_NOTE = ( + "Descriptive label inferred from geometry; Cambridge publishes an official " + "name only for the H-series." +) + + +def parse(model: str): + m = re.match(r"ASSY-([0-9]+D?)-(.+)", model) + series, variant = m.group(1), m.group(2) + # The family is always the single leading letter (E/P/F/H/L/M). Match one + # char only, so "Fb" -> "F" (not "Fb") while "H10b" -> "H". + letter = variant[0] + return series, letter, variant + + +def variant_sort_key(variant: str): + # Natural order: H2 < H3 < H9 < H10 < H10b; P-1 < P-2; F8-0 < F8-1. + nums = [int(n) for n in re.findall(r"\d+", variant)] + return (re.match(r"[A-Za-z]+", variant).group(0), nums, variant) + + +def main() -> None: + root = Path(__file__).resolve().parent.parent + probes = [] + for path in sorted((root / MANUFACTURER).glob("*/*.json")): + data = json.load(open(path))["probes"][0] + model = data["annotations"]["model_name"] + channels = len(data["contact_positions"]) + shank_ids = data.get("shank_ids") + shanks = len(set(shank_ids)) if shank_ids else 1 + series, letter, variant = parse(model) + # Bucket the lone 63-channel probe with 64. + channel_bucket = 64 if channels == 63 else channels + probes.append((letter, channel_bucket, variant, model, series, shanks)) + + def series_sort(series): + modality, _, connector = SERIES[series] + return (0 if modality == "Acute" else 1, int(re.sub(r"\D", "", series)), connector) + + hierarchy = [] + for letter in TYPE_ORDER: + fam = [p for p in probes if p[0] == letter] + if not fam: + continue + channel_nodes = [] + for channel in sorted(set(p[1] for p in fam)): + in_channel = [p for p in fam if p[1] == channel] + variant_nodes = [] + for variant in sorted(set(p[2] for p in in_channel), key=variant_sort_key): + group = [p for p in in_channel if p[2] == variant] + group.sort(key=lambda p: series_sort(p[4])) + shank_set = set(p[5] for p in group) + if len(shank_set) == 1: + n = next(iter(shank_set)) + label = f"{variant} ({n} shank{'s' if n != 1 else ''})" + else: + label = variant + variant_nodes.append({"label": label, "probes": [p[3] for p in group]}) + channel_nodes.append({"label": f"{channel} channel", "children": variant_nodes}) + node = {"label": TYPE_LABEL[letter], "children": channel_nodes} + if letter != "H": + node["note"] = TYPE_NOTE + hierarchy.append(node) + + out = root / "apps" / "probe-viewer" / "src" / "grouping" / f"{MANUFACTURER}.json" + out.write_text(json.dumps({"hierarchy": hierarchy}, indent=2) + "\n") + + placed = sum(len(v["probes"]) for fam in hierarchy for ch in fam["children"] for v in ch["children"]) + print(f"wrote {out}") + print(f"placed {placed} / {len(probes)} probes") + # Textual preview + leaf-size audit. + for fam in hierarchy: + print(f"\n{fam['label']}") + for ch in fam["children"]: + print(f" {ch['label']}") + for v in ch["children"]: + print(f" {v['label']:14} [{len(v['probes'])}] {', '.join(v['probes'])}") + + +if __name__ == "__main__": + main() From 8031ee0ff24a91c5832f47a462bb1c9a25e9ed6f Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 10 Jun 2026 11:44:18 -0600 Subject: [PATCH 2/2] remove script --- .../src/grouping/cambridgeneurotech.json | 56 +++++++ scripts/generate_cambridge_grouping.py | 142 ------------------ 2 files changed, 56 insertions(+), 142 deletions(-) delete mode 100644 scripts/generate_cambridge_grouping.py diff --git a/apps/probe-viewer/src/grouping/cambridgeneurotech.json b/apps/probe-viewer/src/grouping/cambridgeneurotech.json index f7d9e14..30c6997 100644 --- a/apps/probe-viewer/src/grouping/cambridgeneurotech.json +++ b/apps/probe-viewer/src/grouping/cambridgeneurotech.json @@ -8,6 +8,7 @@ "children": [ { "label": "H1b (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H1b", "ASSY-116-H1b" @@ -15,6 +16,7 @@ }, { "label": "H4 (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H4", "ASSY-116-H4", @@ -23,6 +25,7 @@ }, { "label": "H6b (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H6b", "ASSY-116-H6b", @@ -31,6 +34,7 @@ }, { "label": "H7b (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H7b", "ASSY-116-H7b" @@ -38,6 +42,7 @@ }, { "label": "H8b (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H8b", "ASSY-116-H8b" @@ -45,6 +50,7 @@ }, { "label": "H10b (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H10b", "ASSY-116-H10b", @@ -58,6 +64,7 @@ "children": [ { "label": "H1 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-77-H1", "ASSY-156-H1", @@ -66,6 +73,7 @@ }, { "label": "H2 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-77-H2", "ASSY-156-H2", @@ -77,6 +85,7 @@ }, { "label": "H3 (1 shank)", + "collapsible": false, "probes": [ "ASSY-37-H3", "ASSY-77-H3", @@ -89,6 +98,7 @@ }, { "label": "H5 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-H5", "ASSY-156-H5", @@ -100,6 +110,7 @@ }, { "label": "H6 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-77-H6", "ASSY-156-H6", @@ -111,6 +122,7 @@ }, { "label": "H7 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-77-H7", "ASSY-156-H7", @@ -122,6 +134,7 @@ }, { "label": "H8 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-77-H8", "ASSY-156-H8", @@ -133,6 +146,7 @@ }, { "label": "H9 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-H9", "ASSY-156-H9", @@ -144,6 +158,7 @@ }, { "label": "H10 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-77-H10", "ASSY-156-H10", @@ -160,60 +175,70 @@ "children": [ { "label": "H7 (1 shank)", + "collapsible": false, "probes": [ "ASSY-325D-H7" ] }, { "label": "H10 (1 shank)", + "collapsible": false, "probes": [ "ASSY-325D-H10" ] }, { "label": "H12 (1 shank)", + "collapsible": false, "probes": [ "ASSY-350-H12" ] }, { "label": "H13 (1 shank)", + "collapsible": false, "probes": [ "ASSY-350-H13" ] }, { "label": "H14-1 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-350-H14-1" ] }, { "label": "H14-2 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-350-H14-2" ] }, { "label": "H15 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-350-H15" ] }, { "label": "H15_2 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-350-H15_2" ] }, { "label": "H16 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-350-H16" ] }, { "label": "H20 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-350-H20" ] @@ -230,6 +255,7 @@ "children": [ { "label": "P-1 (1 shank)", + "collapsible": false, "probes": [ "ASSY-1-P-1", "ASSY-79-P-1" @@ -237,6 +263,7 @@ }, { "label": "P-2 (1 shank)", + "collapsible": false, "probes": [ "ASSY-1-P-2", "ASSY-79-P-2" @@ -249,6 +276,7 @@ "children": [ { "label": "P-1 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-37-P-1", "ASSY-116-P-1", @@ -257,6 +285,7 @@ }, { "label": "P-2 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-37-P-2", "ASSY-116-P-2", @@ -270,6 +299,7 @@ "children": [ { "label": "P-1 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-77-P-1", "ASSY-156-P-1", @@ -281,6 +311,7 @@ }, { "label": "P-2 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-77-P-2", "ASSY-156-P-2", @@ -297,12 +328,14 @@ "children": [ { "label": "P-1 (1 shank)", + "collapsible": false, "probes": [ "ASSY-325D-P-1" ] }, { "label": "P-2 (1 shank)", + "collapsible": false, "probes": [ "ASSY-325D-P-2" ] @@ -320,6 +353,7 @@ "children": [ { "label": "E-1 (1 shank)", + "collapsible": false, "probes": [ "ASSY-1-E-1", "ASSY-79-E-1" @@ -327,6 +361,7 @@ }, { "label": "E-2 (1 shank)", + "collapsible": false, "probes": [ "ASSY-1-E-2", "ASSY-79-E-2" @@ -339,6 +374,7 @@ "children": [ { "label": "E-1 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-37-E-1", "ASSY-116-E-1", @@ -347,6 +383,7 @@ }, { "label": "E-2 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-37-E-2", "ASSY-116-E-2", @@ -360,6 +397,7 @@ "children": [ { "label": "E-1 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-77-E-1", "ASSY-156-E-1", @@ -371,6 +409,7 @@ }, { "label": "E-2 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-77-E-2", "ASSY-156-E-2", @@ -387,12 +426,14 @@ "children": [ { "label": "E-1 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-325D-E-1" ] }, { "label": "E-2 (4 shanks)", + "collapsible": false, "probes": [ "ASSY-325D-E-2" ] @@ -410,6 +451,7 @@ "children": [ { "label": "Fb (3 shanks)", + "collapsible": false, "probes": [ "ASSY-37-Fb", "ASSY-116-Fb", @@ -423,6 +465,7 @@ "children": [ { "label": "F (6 shanks)", + "collapsible": false, "probes": [ "ASSY-77-F", "ASSY-156-F", @@ -439,24 +482,28 @@ "children": [ { "label": "F (6 shanks)", + "collapsible": false, "probes": [ "ASSY-325D-F" ] }, { "label": "F8-0 (8 shanks)", + "collapsible": false, "probes": [ "ASSY-350-F8-0" ] }, { "label": "F8-1 (8 shanks)", + "collapsible": false, "probes": [ "ASSY-350-F8-1" ] }, { "label": "F8-2 (8 shanks)", + "collapsible": false, "probes": [ "ASSY-350-F8-2" ] @@ -474,6 +521,7 @@ "children": [ { "label": "L1 (1 shank)", + "collapsible": false, "probes": [ "ASSY-79-L1" ] @@ -485,6 +533,7 @@ "children": [ { "label": "L2 (2 shanks)", + "collapsible": false, "probes": [ "ASSY-37-L2", "ASSY-116-L2" @@ -497,6 +546,7 @@ "children": [ { "label": "L3 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-L3", "ASSY-156-L3", @@ -513,12 +563,14 @@ "children": [ { "label": "L13 (1 shank)", + "collapsible": false, "probes": [ "ASSY-350-L13" ] }, { "label": "L14 (8 shanks)", + "collapsible": false, "probes": [ "ASSY-350-L14" ] @@ -536,6 +588,7 @@ "children": [ { "label": "M1v1 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-M1v1", "ASSY-156-M1v1", @@ -546,6 +599,7 @@ }, { "label": "M1v2 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-M1v2", "ASSY-156-M1v2", @@ -557,6 +611,7 @@ }, { "label": "M2v1 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-M2v1", "ASSY-156-M2v1", @@ -567,6 +622,7 @@ }, { "label": "M2v2 (1 shank)", + "collapsible": false, "probes": [ "ASSY-77-M2v2", "ASSY-156-M2v2", diff --git a/scripts/generate_cambridge_grouping.py b/scripts/generate_cambridge_grouping.py deleted file mode 100644 index f7287b2..0000000 --- a/scripts/generate_cambridge_grouping.py +++ /dev/null @@ -1,142 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [] -# /// -"""Generate the explicit sidebar-grouping config for Cambridge NeuroTech. - -The probe-viewer sidebar groups one manufacturer from a hand-curated JSON tree -(see apps/probe-viewer/src/grouping/). This script emits that JSON for Cambridge -NeuroTech so every one of its ~171 models is placed (no "Ungrouped" bucket), -then it can be hand-tuned. It reads only the committed probe JSON (stdlib only). - -Decode of the model id ASSY--: - - series number -> channel count + acute/chronic + connector (SERIES table) - - variant letter -> electrode-layout family (E/P/F/H/L/M) - - full variant -> the specific geometry (H3, P-1, M1v1, F8-0, ...) - -Hierarchy (chosen after web research on how these probes are cited): the type -letter is the top level (researchers cite the geometry, e.g. "H3", and Cambridge -brands the H family), then channel count, then the specific geometry variant as -the leaf, whose probes are the packaging options (the different ASSY numbers). - -Run with: uv run scripts/generate_cambridge_grouping.py -""" - -import json -import re -from pathlib import Path - -MANUFACTURER = "cambridgeneurotech" - -# series number -> (modality, channels, connector). From the Cambridge NeuroTech -# probe-maps table (cambridgeneurotech.com/probe-maps); used for sort order only. -SERIES = { - "1": ("Acute", 16, "Samtec"), - "37": ("Acute", 32, "Samtec"), - "77": ("Acute", 64, "Samtec"), - "79": ("Chronic", 16, "Omnetics"), - "116": ("Chronic", 32, "Omnetics"), - "156": ("Chronic", 64, "Omnetics"), - "158": ("Chronic", 64, "Omnetics"), - "196": ("Chronic", 32, "Molex"), - "236": ("Chronic", 64, "Molex"), - "276": ("Chronic", 64, "TDT Zif-Clip"), - "325": ("Chronic", 64, "Intan (digital)"), - "325D": ("Chronic", 128, "Intan (digital)"), - "350": ("Chronic", 128, "Intan (digital)"), -} - -# Type-letter families, in display order. H first (largest, the only family -# Cambridge brands). The parenthetical for E/P/F/L/M is a descriptive label -# inferred from geometry, not an official Cambridge name (only "H-series" is -# documented); recorded in TYPE_NOTE below. -TYPE_ORDER = ["H", "P", "E", "F", "L", "M"] -TYPE_LABEL = { - "H": "H (high-resolution / dense recording)", - "P": "P (compact array)", - "E": "E (compact edge array)", - "F": "F (fine-pitch multi-shank)", - "L": "L (linear / laminar)", - "M": "M (single-shank)", -} -TYPE_NOTE = ( - "Descriptive label inferred from geometry; Cambridge publishes an official " - "name only for the H-series." -) - - -def parse(model: str): - m = re.match(r"ASSY-([0-9]+D?)-(.+)", model) - series, variant = m.group(1), m.group(2) - # The family is always the single leading letter (E/P/F/H/L/M). Match one - # char only, so "Fb" -> "F" (not "Fb") while "H10b" -> "H". - letter = variant[0] - return series, letter, variant - - -def variant_sort_key(variant: str): - # Natural order: H2 < H3 < H9 < H10 < H10b; P-1 < P-2; F8-0 < F8-1. - nums = [int(n) for n in re.findall(r"\d+", variant)] - return (re.match(r"[A-Za-z]+", variant).group(0), nums, variant) - - -def main() -> None: - root = Path(__file__).resolve().parent.parent - probes = [] - for path in sorted((root / MANUFACTURER).glob("*/*.json")): - data = json.load(open(path))["probes"][0] - model = data["annotations"]["model_name"] - channels = len(data["contact_positions"]) - shank_ids = data.get("shank_ids") - shanks = len(set(shank_ids)) if shank_ids else 1 - series, letter, variant = parse(model) - # Bucket the lone 63-channel probe with 64. - channel_bucket = 64 if channels == 63 else channels - probes.append((letter, channel_bucket, variant, model, series, shanks)) - - def series_sort(series): - modality, _, connector = SERIES[series] - return (0 if modality == "Acute" else 1, int(re.sub(r"\D", "", series)), connector) - - hierarchy = [] - for letter in TYPE_ORDER: - fam = [p for p in probes if p[0] == letter] - if not fam: - continue - channel_nodes = [] - for channel in sorted(set(p[1] for p in fam)): - in_channel = [p for p in fam if p[1] == channel] - variant_nodes = [] - for variant in sorted(set(p[2] for p in in_channel), key=variant_sort_key): - group = [p for p in in_channel if p[2] == variant] - group.sort(key=lambda p: series_sort(p[4])) - shank_set = set(p[5] for p in group) - if len(shank_set) == 1: - n = next(iter(shank_set)) - label = f"{variant} ({n} shank{'s' if n != 1 else ''})" - else: - label = variant - variant_nodes.append({"label": label, "probes": [p[3] for p in group]}) - channel_nodes.append({"label": f"{channel} channel", "children": variant_nodes}) - node = {"label": TYPE_LABEL[letter], "children": channel_nodes} - if letter != "H": - node["note"] = TYPE_NOTE - hierarchy.append(node) - - out = root / "apps" / "probe-viewer" / "src" / "grouping" / f"{MANUFACTURER}.json" - out.write_text(json.dumps({"hierarchy": hierarchy}, indent=2) + "\n") - - placed = sum(len(v["probes"]) for fam in hierarchy for ch in fam["children"] for v in ch["children"]) - print(f"wrote {out}") - print(f"placed {placed} / {len(probes)} probes") - # Textual preview + leaf-size audit. - for fam in hierarchy: - print(f"\n{fam['label']}") - for ch in fam["children"]: - print(f" {ch['label']}") - for v in ch["children"]: - print(f" {v['label']:14} [{len(v['probes'])}] {', '.join(v['probes'])}") - - -if __name__ == "__main__": - main()