Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,58 @@ Every sub-package contains a file named `ndi_matlab_python_bridge.yaml`.
- **Internal Access:** Use 0-based indexing for internal Python data structures (lists, NumPy arrays).
- **Formatting:** Code must pass `black` and `ruff check --fix` before completion.

## 5. Directory Mapping Reference
## 5. CI Lint & Test Commands

Before pushing any changes, you **must** run these commands and ensure they all pass. These are the same checks CI runs.

### Formatting (Black)

```bash
black --check src/ tests/
```

To auto-fix formatting issues:

```bash
black src/ tests/
```

Configuration is in `pyproject.toml`: line-length = 100, target-version = py310/py311/py312.

### Linting (Ruff)

```bash
ruff check src/ tests/
```

To auto-fix what ruff can:

```bash
ruff check --fix src/ tests/
```

Configuration is in `pyproject.toml` under `[tool.ruff]` and `[tool.ruff.lint]`.

### Tests

```bash
pytest tests/ -v --tb=short
```

Symmetry tests (cross-language MATLAB/Python parity) are excluded from the default run and are invoked separately in CI:

```bash
pytest tests/symmetry/make_artifacts/ -v --tb=short
pytest tests/symmetry/read_artifacts/ -v --tb=short
```

### Quick pre-push checklist

```bash
black src/ tests/ && ruff check src/ tests/ && pytest tests/ -x -q
```

## 6. Directory Mapping Reference

- **MATLAB Source:** `VH-ndi_gui_Lab/NDI-matlab` (GitHub)
- **Python Target:** `src/ndi/[namespace]/` (Mirrors MATLAB `+namespace/`)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ classifiers = [
requires-python = ">=3.10"
dependencies = [
"did @ git+https://github.com/VH-Lab/DID-python.git@main",
"ndr @ git+https://github.com/VH-lab/NDR-python.git@main",
"ndr[formats] @ git+https://github.com/VH-lab/NDR-python.git@main",
"vhlab-toolbox-python @ git+https://github.com/VH-Lab/vhlab-toolbox-python.git@main",
"ndi-compress @ git+https://github.com/Waltham-Data-Science/NDI-compress-python.git@main",
"numpy>=1.20.0",
Expand Down
6 changes: 6 additions & 0 deletions src/ndi/class_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def _build_registry() -> dict[str, type]:
from .setup.daq.reader.mfdaq.stimulus.nielsenvisintan import (
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisintan,
)
from .setup.daq.reader.mfdaq.stimulus.nielsenvisneuropixelsglx import (
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisneuropixelsglx,
)
from .setup.daq.reader.mfdaq.stimulus.vhaudreybpod import (
ndi_setup_daq_reader_mfdaq_stimulus_VHAudreyBPod,
)
Expand Down Expand Up @@ -71,6 +74,7 @@ def _build_registry() -> dict[str, type]:
ndi_daq_reader_mfdaq_spikegadgets,
ndi_setup_daq_reader_mfdaq_stimulus_vhlabvisspike2,
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisintan,
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisneuropixelsglx,
ndi_setup_daq_reader_mfdaq_stimulus_VHAudreyBPod,
):
registry[cls.NDI_DAQREADER_CLASS] = cls
Expand All @@ -81,6 +85,8 @@ def _build_registry() -> dict[str, type]:
# File navigators
registry[ndi_file_navigator.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator
registry[ndi_file_navigator_epochdir.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator_epochdir
# Custom lab-specific navigators mapped to epochdir until dedicated classes exist
registry["ndi.setup.file.navigator.vhlab_np_epochdir"] = ndi_file_navigator_epochdir

return registry

Expand Down
57 changes: 55 additions & 2 deletions src/ndi/daq/daqsystemstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,16 @@ def parse(cls, devstr: str) -> ndi_daq_daqsystemstring:
channeltype = match.group(1)
numspec = match.group(2)

# Check for threshold suffix (e.g., '_t2.5' in 'aep1-3_t2.5')
threshold_str = ""
t_idx = numspec.find("_t")
if t_idx != -1:
threshold_str = numspec[t_idx:]
numspec = numspec[:t_idx]

channellist = _parse_channel_numbers(numspec)
if threshold_str:
channeltype = channeltype + threshold_str
channels.append((channeltype, channellist))

return cls(devicename=devicename, channels=channels)
Expand All @@ -96,8 +105,7 @@ def devicestring(self) -> str:
if not channellist:
parts.append(channeltype)
else:
numstr = _format_channel_numbers(channellist)
parts.append(f"{channeltype}{numstr}")
parts.append(ndi_daq_daqsystemstring.channeltype2str(channeltype, channellist))

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

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

@staticmethod
def channeltype2str(ct: str, channellist: list[int]) -> str:
"""
Build a device string segment from a channeltype and channel list.

Handles threshold suffixes (e.g., ``_t2.5``) by placing the channel
numbers between the base type and the suffix.

Args:
ct: Channel type string, optionally with threshold suffix
(e.g., ``'aep'`` or ``'aep_t2.5'``)
channellist: List of channel numbers

Returns:
Device string segment (e.g., ``'aep1-3_t2.5'``)
"""
t_idx = ct.find("_t")
if t_idx != -1:
base = ct[:t_idx]
threshold_str = ct[t_idx:]
return f"{base}{_format_channel_numbers(channellist)}{threshold_str}"
return f"{ct}{_format_channel_numbers(channellist)}"

@staticmethod
def parse_analog_event_channeltype(ct: str) -> tuple[str, float]:
"""
Extract base type and threshold from a channel type string.

Given a channel type string like ``'aep_t2.5'``, returns the base
type (``'aep'``) and threshold (``2.5``). If no threshold suffix
is present, threshold is ``0.0``.

Args:
ct: Channel type string (e.g., ``'aep_t2.5'``, ``'aimp'``)

Returns:
Tuple of (base_type, threshold)
"""
t_idx = ct.find("_t")
if t_idx != -1:
base_type = ct[:t_idx]
threshold = float(ct[t_idx + 2 :])
return base_type, threshold
return ct, 0.0

def __eq__(self, other) -> bool:
if not isinstance(other, ndi_daq_daqsystemstring):
return False
Expand Down
Loading
Loading