Skip to content

Commit 5534d9e

Browse files
Merge pull request #2 from VH-Lab/claude/complete-intan-port-9bQeC
Port MATLAB NDR functions to Python and rename base reader class
2 parents af13e6a + cf3a4ce commit 5534d9e

132 files changed

Lines changed: 56878 additions & 227 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/PORT_STATUS.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# NDR-python Port Status
2+
3+
Status of the MATLAB → Python port of [NDR-matlab](https://github.com/VH-Lab/NDR-matlab).
4+
5+
## Naming Convention
6+
7+
Python class names are a mechanical mapping of the fully-qualified MATLAB class name,
8+
applying the **Mirror Rule**:
9+
10+
1. Periods (`.`) are replaced with single underscores (`_`).
11+
2. Existing underscores (`_`) in the MATLAB name are replaced with double underscores (`__`).
12+
13+
| MATLAB qualified name | Python module | Python class |
14+
|---|---|---|
15+
| `ndr.reader` | `ndr.reader_wrapper` | `ndr_reader` |
16+
| `ndr.reader.base` | `ndr.reader.base` | `ndr_reader_base` |
17+
| `ndr.reader.intan_rhd` | `ndr.reader.intan_rhd` | `ndr_reader_intan__rhd` |
18+
| `ndr.reader.ced_smr` | `ndr.reader.ced_smr` | `ndr_reader_ced__smr` |
19+
| `ndr.reader.axon_abf` | `ndr.reader.axon_abf` | `ndr_reader_axon__abf` |
20+
| `ndr.reader.neo` | `ndr.reader.neo` | `ndr_reader_neo` |
21+
| `ndr.reader.spikegadgets_rec` | `ndr.reader.spikegadgets_rec` | `ndr_reader_spikegadgets__rec` |
22+
| `ndr.reader.tdt_sev` | `ndr.reader.tdt_sev` | `ndr_reader_tdt__sev` |
23+
| `ndr.reader.bjg` | `ndr.reader.bjg` | `ndr_reader_bjg` |
24+
| `ndr.reader.dabrowska` | `ndr.reader.dabrowska` | `ndr_reader_dabrowska` |
25+
| `ndr.reader.whitematter` | `ndr.reader.whitematter` | `ndr_reader_whitematter` |
26+
| `ndr.reader.somecompany_someformat` | `ndr.reader.somecompany_someformat` | `ndr_reader_somecompany__someformat` |
27+
28+
## Reader Status
29+
30+
| Reader | getchannelsepoch | t0\_t1 | samplerate | readchannels\_epochsamples | readevents\_epochsamples\_native | read | Tests |
31+
|---|---|---|---|---|---|---|---|
32+
| **ndr\_reader\_intan\_\_rhd** | Yes | Yes | Yes | Yes (single-file) | Stub (empty) | Yes | 6 pass |
33+
| **ndr\_reader\_ced\_\_smr** | Yes | Yes | Yes | Yes | Yes | Yes (via base) | 14 pass |
34+
| **ndr\_reader\_axon\_\_abf** | Yes | Yes | Yes | Yes | Stub (empty) | Yes (via base) | 6 pass |
35+
| **ndr\_reader\_neo** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | 24 xfail |
36+
| **ndr\_reader\_spikegadgets\_\_rec** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | xfail |
37+
| **ndr\_reader\_tdt\_\_sev** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
38+
| **ndr\_reader\_bjg** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
39+
| **ndr\_reader\_dabrowska** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
40+
| **ndr\_reader\_whitematter** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
41+
42+
**Legend:**
43+
- **Yes** — Fully implemented and tested with example data
44+
- **Stub (empty)** — Returns empty arrays / default values; no errors raised
45+
- **NotImplementedError** — Raises an exception; not yet implemented
46+
- **Stub** — Inherits base class default (empty list, `[[nan,nan]]`, etc.)
47+
48+
## Format Parsers
49+
50+
Low-level format parsers (under `ndr.format.*`) that read binary files:
51+
52+
| Format | Module | Status |
53+
|---|---|---|
54+
| Intan RHD | `ndr.format.intan` | Implemented (header + single-file data reader) |
55+
| CED SMR/SON | `ndr.format.ced` | Implemented (via `neo` library) |
56+
| Axon ABF | `ndr.format.axon` | Implemented (via `pyabf` library) |
57+
| SpikeGadgets REC | `ndr.format.spikegadgets` | Implemented (config, analog, digital, trode) |
58+
| TDT SEV | `ndr.format.tdt` | Implemented (header + channel reader) |
59+
| BJG | `ndr.format.bjg` | Implemented (header + data reader) |
60+
| Dabrowska | `ndr.format.dabrowska` | Implemented (header + data reader) |
61+
| WhiteMatter | `ndr.format.whitematter` | Implemented (header + data reader) |
62+
| Neo / Blackrock | `ndr.format.neo` | Implemented (utilities) |
63+
| Binary Matrix | `ndr.format.binarymatrix` | Implemented |
64+
| Text Signal | `ndr.format.textSignal` | Implemented |
65+
66+
Note: For SpikeGadgets, TDT, BJG, Dabrowska, and WhiteMatter, the format parsers are implemented but the reader classes have not yet been wired up to use them.
67+
68+
## Reader Wrapper
69+
70+
The top-level `ndr_reader` class (`reader_wrapper.py`) wraps any format-specific reader and adds:
71+
72+
| Feature | Status |
73+
|---|---|
74+
| `read()` convenience method | Implemented |
75+
| `readevents_epochsamples()` with derived events (dep, den, dimp, dimn) | Implemented |
76+
| Delegation to underlying reader | Implemented |
77+
78+
## External Dependencies
79+
80+
| Dependency | Used by | Purpose |
81+
|---|---|---|
82+
| `neo` | ndr\_reader\_ced\_\_smr, ndr\_reader\_neo | Read CED SMR/SON and Blackrock files |
83+
| `pyabf` | ndr\_reader\_axon\_\_abf | Read Axon Binary Format files |
84+
| `numpy` | All readers | Array operations |
85+
86+
## Test Summary
87+
88+
```
89+
38 passed, 28 xfailed, 13 skipped, 2 failed (pre-existing spikegadgets format test issues)
90+
```
91+
92+
Example data files are included in `src/ndr/example_data/` for Intan (.rhd), CED (.smr), Axon (.abf), SpikeGadgets (.rec), and Blackrock (.nev, .ns2).

docs/developer_notes/PYTHON_PORTING_GUIDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ Function and class names must match MATLAB exactly.
1717
- **Case Preservation:** Use `readchannels_epochsamples`, not `read_channels_epoch_samples`.
1818
- **Directory Parity:** Python file paths must mirror MATLAB `+namespace` paths
1919
(e.g., `+ndr/+reader` -> `src/ndr/reader/`).
20+
- **Class Name Mirror Rule:** Python class names are derived from the fully-qualified
21+
MATLAB class name by applying two substitutions:
22+
1. Periods (`.`) are replaced with single underscores (`_`).
23+
2. Existing underscores (`_`) in the MATLAB name are replaced with double
24+
underscores (`__`).
25+
26+
Examples:
27+
| MATLAB qualified name | Python class name |
28+
|--------------------------------------|----------------------------------------|
29+
| `ndr.reader` | `ndr_reader` |
30+
| `ndr.reader.base` | `ndr_reader_base` |
31+
| `ndr.reader.intan_rhd` | `ndr_reader_intan__rhd` |
32+
| `ndr.reader.ced_smr` | `ndr_reader_ced__smr` |
33+
| `ndr.reader.axon_abf` | `ndr_reader_axon__abf` |
34+
| `ndr.reader.somecompany_someformat` | `ndr_reader_somecompany__someformat` |
2035

2136
## 3. The Porting Workflow (The Bridge Protocol)
2237
1. **Check the Bridge:** Open the `ndr_matlab_python_bridge.yaml` in the target package.

src/ndr/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
A Python port of NDR-matlab (https://github.com/VH-Lab/NDR-matlab).
44
"""
55

6-
from ndr.reader_wrapper import Reader as reader
6+
from ndr.reader_wrapper import ndr_reader as reader
77

88
__version__ = "0.1.0"

src/ndr/data/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@
33
Port of +ndr/+data/
44
"""
55

6+
from ndr.data.assign import assign
67
from ndr.data.colvec import colvec
78
from ndr.data.rowvec import rowvec
9+
from ndr.data.struct2namevaluepair import struct2namevaluepair

src/ndr/data/assign.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Assign name/value pairs into a target namespace (dict).
2+
3+
Port of +ndr/+data/assign.m
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
from ndr.data.struct2namevaluepair import struct2namevaluepair
11+
12+
13+
def assign(target: dict[str, Any], *args: Any) -> dict[str, Any]:
14+
"""Apply a list of name/value pair assignments to *target*.
15+
16+
In MATLAB ``ndr.data.assign`` uses ``assignin('caller', ...)`` to inject
17+
variables into the caller's workspace. In Python the idiomatic
18+
equivalent is to update a dictionary (typically ``locals()`` or an
19+
options dict) and return it.
20+
21+
Parameters
22+
----------
23+
target : dict
24+
The dictionary to update with the supplied name/value pairs.
25+
*args
26+
Either a single ``dict`` (struct equivalent), a single ``list``
27+
of alternating name/value items, or inline alternating
28+
``name, value, name, value, ...`` arguments.
29+
30+
Returns
31+
-------
32+
dict
33+
The updated *target* dictionary (same object, mutated in-place).
34+
35+
Examples
36+
--------
37+
>>> opts = {'z': 0}
38+
>>> assign(opts, 'z', 4)
39+
{'z': 4}
40+
41+
>>> assign({}, {'a': 1, 'b': 2})
42+
{'a': 1, 'b': 2}
43+
44+
>>> assign({}, ['x', 10, 'y', 20])
45+
{'x': 10, 'y': 20}
46+
"""
47+
# Normalise a single-argument form (dict or list) into a flat sequence
48+
if len(args) == 1:
49+
arg = args[0]
50+
if isinstance(arg, dict):
51+
flat: list[Any] = struct2namevaluepair(arg)
52+
elif isinstance(arg, (list, tuple)):
53+
flat = list(arg)
54+
else:
55+
raise TypeError("A single argument must be a dict or a list of name/value pairs.")
56+
else:
57+
flat = list(args)
58+
59+
names = flat[0::2]
60+
values = flat[1::2]
61+
62+
for name, value in zip(names, values):
63+
target[name] = value
64+
65+
return target
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
project_metadata:
2+
bridge_version: "1.1"
3+
naming_policy: "Strict MATLAB Mirror"
4+
indexing_policy: "Semantic Parity (1-based for user concepts, 0-based for internal data)"
5+
6+
functions:
7+
- name: assign
8+
matlab_path: "+ndr/+data/assign.m"
9+
python_path: "ndr/data/assign.py"
10+
input_arguments:
11+
- name: target
12+
type_python: "dict[str, Any]"
13+
- name: args
14+
type_python: "*Any"
15+
output_arguments:
16+
- name: target
17+
type_python: "dict[str, Any]"
18+
19+
- name: colvec
20+
matlab_path: "+ndr/+data/colvec.m"
21+
python_path: "ndr/data/colvec.py"
22+
input_arguments:
23+
- name: x
24+
type_python: "numpy.ndarray"
25+
output_arguments:
26+
- name: y
27+
type_python: "numpy.ndarray"
28+
29+
- name: rowvec
30+
matlab_path: "+ndr/+data/rowvec.m"
31+
python_path: "ndr/data/rowvec.py"
32+
input_arguments:
33+
- name: x
34+
type_python: "numpy.ndarray"
35+
output_arguments:
36+
- name: y
37+
type_python: "numpy.ndarray"
38+
39+
- name: struct2namevaluepair
40+
matlab_path: "+ndr/+data/struct2namevaluepair.m"
41+
python_path: "ndr/data/struct2namevaluepair.py"
42+
input_arguments:
43+
- name: thestruct
44+
type_python: "dict[str, Any]"
45+
output_arguments:
46+
- name: nv
47+
type_python: "list[Any]"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Convert a dict to a flat list of name/value pairs.
2+
3+
Port of +ndr/+data/struct2namevaluepair.m
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
11+
def struct2namevaluepair(thestruct: dict[str, Any]) -> list[Any]:
12+
"""Convert a dictionary to a flat list of name/value pairs.
13+
14+
This is useful for passing name/value pairs to functions that accept
15+
them as extra keyword arguments. Each key of the dictionary is used as
16+
the 'name', and the corresponding value is used as the 'value'.
17+
18+
Parameters
19+
----------
20+
thestruct : dict
21+
Input dictionary mapping parameter names to values.
22+
23+
Returns
24+
-------
25+
list
26+
Flat list alternating between keys and values,
27+
e.g. ``['param1', 1, 'param2', 2]``.
28+
29+
Examples
30+
--------
31+
>>> struct2namevaluepair({'param1': 1, 'param2': 2})
32+
['param1', 1, 'param2', 2]
33+
"""
34+
if not thestruct:
35+
return []
36+
37+
nv: list[Any] = []
38+
for key, value in thestruct.items():
39+
nv.append(key)
40+
nv.append(value)
41+
return nv

0 commit comments

Comments
 (0)