Skip to content

Commit 56bce32

Browse files
committed
new file
1 parent 07a2dc4 commit 56bce32

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""
2+
Post-process neuropixels_probe_features.json after syncing from ProbeTable.
3+
4+
Derives two mappings from the catalogue and writes them back into the JSON:
5+
6+
- z_imro_format_type_to_imro_format: IMRO type code -> IMRO format name
7+
(e.g. "0" -> "imro_np1000", "1110" -> "imro_np1110")
8+
9+
- z_imro_format_type_to_part_number: IMRO type code -> canonical probe part number
10+
(e.g. "0" -> "NP1000", "1110" -> "NP1110")
11+
12+
This script is called by the GitHub Action workflow that syncs probe_features.json
13+
from billkarsh/ProbeTable, and can also be run standalone.
14+
"""
15+
16+
import json
17+
import re
18+
from pathlib import Path
19+
20+
PROBE_FEATURES_PATH = (
21+
Path(__file__).absolute().parent
22+
/ "../src/probeinterface/resources/neuropixels_probe_features.json"
23+
)
24+
25+
26+
def _parse_type_values_from_val_def(val_def: str) -> list[str]:
27+
"""Extract IMRO type code(s) from a val_def string.
28+
29+
Two patterns in ProbeTable:
30+
type:{0,1020,1030,...} -> set of values
31+
type:1110 -> single value
32+
"""
33+
match = re.match(r"type:\{([^}]+)\}", val_def)
34+
if match:
35+
return [v.strip() for v in match.group(1).split(",")]
36+
37+
match = re.match(r"type:(\d+)", val_def)
38+
if match:
39+
return [match.group(1)]
40+
41+
raise ValueError(f"Cannot parse type from val_def: {val_def!r}")
42+
43+
44+
def build_derived_mappings(probe_features: dict) -> tuple[dict, dict]:
45+
"""Build type-to-format and type-to-part-number mappings from the catalogue."""
46+
47+
imro_formats = probe_features["z_imro_formats"]
48+
probes = probe_features["neuropixels_probes"]
49+
50+
# 1. Build type -> format mapping from val_def entries
51+
type_to_format = {}
52+
for key, val_def in imro_formats.items():
53+
if not key.endswith("_val_def"):
54+
continue
55+
# e.g. "imro_np1000_val_def" -> "imro_np1000"
56+
format_name = key.removesuffix("_val_def")
57+
for type_code in _parse_type_values_from_val_def(val_def):
58+
if type_code in type_to_format:
59+
raise ValueError(
60+
f"IMRO type {type_code!r} maps to both "
61+
f"{type_to_format[type_code]!r} and {format_name!r}"
62+
)
63+
type_to_format[type_code] = format_name
64+
65+
# 2. Build type -> canonical part number mapping
66+
# For each type, find probes that use the matching format, then pick
67+
# the first NP-prefixed part number alphabetically.
68+
#
69+
# We also need to verify the candidate actually belongs to this type,
70+
# not just the same format. For example, NP1021 uses imro_np1000 format
71+
# but its IMRO type is not "0". We filter by checking the format's
72+
# val_def includes the type code we're resolving.
73+
74+
# Invert: format -> set of type codes it covers
75+
format_to_types = {}
76+
for type_code, format_name in type_to_format.items():
77+
format_to_types.setdefault(format_name, set()).add(type_code)
78+
79+
type_to_part_number = {}
80+
for type_code, format_name in sorted(type_to_format.items()):
81+
candidates = [
82+
pn
83+
for pn, spec in probes.items()
84+
if spec.get("imro_table_format_type") == format_name
85+
]
86+
87+
# Prefer a probe whose part number contains the type code (e.g. NP1020 for type "1020").
88+
# This matters because many probes share the same IMRO format but have different
89+
# physical geometries (e.g. NP1000 has 960 contacts, NP1020 has 2496).
90+
exact_matches = sorted(
91+
pn for pn in candidates if pn.startswith("NP") and type_code in pn
92+
)
93+
if exact_matches:
94+
type_to_part_number[type_code] = exact_matches[0]
95+
continue
96+
97+
# Fall back to first NP-prefixed name alphabetically
98+
np_candidates = sorted(pn for pn in candidates if pn.startswith("NP"))
99+
other_candidates = sorted(pn for pn in candidates if not pn.startswith("NP"))
100+
ordered = np_candidates + other_candidates
101+
102+
if ordered:
103+
type_to_part_number[type_code] = ordered[0]
104+
105+
return type_to_format, type_to_part_number
106+
107+
108+
def postprocess(filepath: Path = PROBE_FEATURES_PATH) -> None:
109+
filepath = filepath.resolve()
110+
with open(filepath) as f:
111+
probe_features = json.load(f)
112+
113+
type_to_format, type_to_part_number = build_derived_mappings(probe_features)
114+
115+
probe_features["z_imro_format_type_to_imro_format"] = dict(sorted(type_to_format.items(), key=lambda kv: int(kv[0])))
116+
probe_features["z_imro_format_type_to_part_number"] = dict(sorted(type_to_part_number.items(), key=lambda kv: int(kv[0])))
117+
118+
with open(filepath, "w") as f:
119+
json.dump(probe_features, f, indent=4)
120+
f.write("\n")
121+
122+
print(f"Wrote derived mappings to {filepath}")
123+
print(f" z_imro_format_type_to_imro_format: {len(type_to_format)} entries")
124+
print(f" z_imro_format_type_to_part_number: {len(type_to_part_number)} entries")
125+
for type_code in sorted(type_to_format, key=int):
126+
pn = type_to_part_number.get(type_code, "???")
127+
print(f" type {type_code:>5s} -> format={type_to_format[type_code]}, part_number={pn}")
128+
129+
130+
if __name__ == "__main__":
131+
postprocess()

0 commit comments

Comments
 (0)