Skip to content

Commit 4cd4f97

Browse files
Merge pull request #54 from Waltham-Data-Science/claude/port-ndi-matlab-changes-9ttly
Add analog event type support with threshold-based detection
2 parents bd3334b + c47e780 commit 4cd4f97

19 files changed

Lines changed: 715 additions & 44 deletions

AGENTS.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,58 @@ Every sub-package contains a file named `ndi_matlab_python_bridge.yaml`.
3232
- **Internal Access:** Use 0-based indexing for internal Python data structures (lists, NumPy arrays).
3333
- **Formatting:** Code must pass `black` and `ruff check --fix` before completion.
3434

35-
## 5. Directory Mapping Reference
35+
## 5. CI Lint & Test Commands
36+
37+
Before pushing any changes, you **must** run these commands and ensure they all pass. These are the same checks CI runs.
38+
39+
### Formatting (Black)
40+
41+
```bash
42+
black --check src/ tests/
43+
```
44+
45+
To auto-fix formatting issues:
46+
47+
```bash
48+
black src/ tests/
49+
```
50+
51+
Configuration is in `pyproject.toml`: line-length = 100, target-version = py310/py311/py312.
52+
53+
### Linting (Ruff)
54+
55+
```bash
56+
ruff check src/ tests/
57+
```
58+
59+
To auto-fix what ruff can:
60+
61+
```bash
62+
ruff check --fix src/ tests/
63+
```
64+
65+
Configuration is in `pyproject.toml` under `[tool.ruff]` and `[tool.ruff.lint]`.
66+
67+
### Tests
68+
69+
```bash
70+
pytest tests/ -v --tb=short
71+
```
72+
73+
Symmetry tests (cross-language MATLAB/Python parity) are excluded from the default run and are invoked separately in CI:
74+
75+
```bash
76+
pytest tests/symmetry/make_artifacts/ -v --tb=short
77+
pytest tests/symmetry/read_artifacts/ -v --tb=short
78+
```
79+
80+
### Quick pre-push checklist
81+
82+
```bash
83+
black src/ tests/ && ruff check src/ tests/ && pytest tests/ -x -q
84+
```
85+
86+
## 6. Directory Mapping Reference
3687

3788
- **MATLAB Source:** `VH-ndi_gui_Lab/NDI-matlab` (GitHub)
3889
- **Python Target:** `src/ndi/[namespace]/` (Mirrors MATLAB `+namespace/`)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ classifiers = [
3636
requires-python = ">=3.10"
3737
dependencies = [
3838
"did @ git+https://github.com/VH-Lab/DID-python.git@main",
39-
"ndr @ git+https://github.com/VH-lab/NDR-python.git@main",
39+
"ndr[formats] @ git+https://github.com/VH-lab/NDR-python.git@main",
4040
"vhlab-toolbox-python @ git+https://github.com/VH-Lab/vhlab-toolbox-python.git@main",
4141
"ndi-compress @ git+https://github.com/Waltham-Data-Science/NDI-compress-python.git@main",
4242
"numpy>=1.20.0",

src/ndi/class_registry.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def _build_registry() -> dict[str, type]:
4343
from .setup.daq.reader.mfdaq.stimulus.nielsenvisintan import (
4444
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisintan,
4545
)
46+
from .setup.daq.reader.mfdaq.stimulus.nielsenvisneuropixelsglx import (
47+
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisneuropixelsglx,
48+
)
4649
from .setup.daq.reader.mfdaq.stimulus.vhaudreybpod import (
4750
ndi_setup_daq_reader_mfdaq_stimulus_VHAudreyBPod,
4851
)
@@ -71,6 +74,7 @@ def _build_registry() -> dict[str, type]:
7174
ndi_daq_reader_mfdaq_spikegadgets,
7275
ndi_setup_daq_reader_mfdaq_stimulus_vhlabvisspike2,
7376
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisintan,
77+
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisneuropixelsglx,
7478
ndi_setup_daq_reader_mfdaq_stimulus_VHAudreyBPod,
7579
):
7680
registry[cls.NDI_DAQREADER_CLASS] = cls
@@ -81,6 +85,8 @@ def _build_registry() -> dict[str, type]:
8185
# File navigators
8286
registry[ndi_file_navigator.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator
8387
registry[ndi_file_navigator_epochdir.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator_epochdir
88+
# Custom lab-specific navigators mapped to epochdir until dedicated classes exist
89+
registry["ndi.setup.file.navigator.vhlab_np_epochdir"] = ndi_file_navigator_epochdir
8490

8591
return registry
8692

src/ndi/daq/daqsystemstring.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,16 @@ def parse(cls, devstr: str) -> ndi_daq_daqsystemstring:
7676
channeltype = match.group(1)
7777
numspec = match.group(2)
7878

79+
# Check for threshold suffix (e.g., '_t2.5' in 'aep1-3_t2.5')
80+
threshold_str = ""
81+
t_idx = numspec.find("_t")
82+
if t_idx != -1:
83+
threshold_str = numspec[t_idx:]
84+
numspec = numspec[:t_idx]
85+
7986
channellist = _parse_channel_numbers(numspec)
87+
if threshold_str:
88+
channeltype = channeltype + threshold_str
8089
channels.append((channeltype, channellist))
8190

8291
return cls(devicename=devicename, channels=channels)
@@ -96,8 +105,7 @@ def devicestring(self) -> str:
96105
if not channellist:
97106
parts.append(channeltype)
98107
else:
99-
numstr = _format_channel_numbers(channellist)
100-
parts.append(f"{channeltype}{numstr}")
108+
parts.append(ndi_daq_daqsystemstring.channeltype2str(channeltype, channellist))
101109

102110
return f"{self.devicename}:{';'.join(parts)}"
103111

@@ -132,6 +140,51 @@ def __str__(self) -> str:
132140
def __repr__(self) -> str:
133141
return f"ndi_daq_daqsystemstring('{self.devicestring()}')"
134142

143+
@staticmethod
144+
def channeltype2str(ct: str, channellist: list[int]) -> str:
145+
"""
146+
Build a device string segment from a channeltype and channel list.
147+
148+
Handles threshold suffixes (e.g., ``_t2.5``) by placing the channel
149+
numbers between the base type and the suffix.
150+
151+
Args:
152+
ct: Channel type string, optionally with threshold suffix
153+
(e.g., ``'aep'`` or ``'aep_t2.5'``)
154+
channellist: List of channel numbers
155+
156+
Returns:
157+
Device string segment (e.g., ``'aep1-3_t2.5'``)
158+
"""
159+
t_idx = ct.find("_t")
160+
if t_idx != -1:
161+
base = ct[:t_idx]
162+
threshold_str = ct[t_idx:]
163+
return f"{base}{_format_channel_numbers(channellist)}{threshold_str}"
164+
return f"{ct}{_format_channel_numbers(channellist)}"
165+
166+
@staticmethod
167+
def parse_analog_event_channeltype(ct: str) -> tuple[str, float]:
168+
"""
169+
Extract base type and threshold from a channel type string.
170+
171+
Given a channel type string like ``'aep_t2.5'``, returns the base
172+
type (``'aep'``) and threshold (``2.5``). If no threshold suffix
173+
is present, threshold is ``0.0``.
174+
175+
Args:
176+
ct: Channel type string (e.g., ``'aep_t2.5'``, ``'aimp'``)
177+
178+
Returns:
179+
Tuple of (base_type, threshold)
180+
"""
181+
t_idx = ct.find("_t")
182+
if t_idx != -1:
183+
base_type = ct[:t_idx]
184+
threshold = float(ct[t_idx + 2 :])
185+
return base_type, threshold
186+
return ct, 0.0
187+
135188
def __eq__(self, other) -> bool:
136189
if not isinstance(other, ndi_daq_daqsystemstring):
137190
return False

0 commit comments

Comments
 (0)