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
92 changes: 92 additions & 0 deletions docs/PORT_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# NDR-python Port Status

Status of the MATLAB → Python port of [NDR-matlab](https://github.com/VH-Lab/NDR-matlab).

## Naming Convention

Python class names are a mechanical mapping of the fully-qualified MATLAB class name,
applying the **Mirror Rule**:

1. Periods (`.`) are replaced with single underscores (`_`).
2. Existing underscores (`_`) in the MATLAB name are replaced with double underscores (`__`).

| MATLAB qualified name | Python module | Python class |
|---|---|---|
| `ndr.reader` | `ndr.reader_wrapper` | `ndr_reader` |
| `ndr.reader.base` | `ndr.reader.base` | `ndr_reader_base` |
| `ndr.reader.intan_rhd` | `ndr.reader.intan_rhd` | `ndr_reader_intan__rhd` |
| `ndr.reader.ced_smr` | `ndr.reader.ced_smr` | `ndr_reader_ced__smr` |
| `ndr.reader.axon_abf` | `ndr.reader.axon_abf` | `ndr_reader_axon__abf` |
| `ndr.reader.neo` | `ndr.reader.neo` | `ndr_reader_neo` |
| `ndr.reader.spikegadgets_rec` | `ndr.reader.spikegadgets_rec` | `ndr_reader_spikegadgets__rec` |
| `ndr.reader.tdt_sev` | `ndr.reader.tdt_sev` | `ndr_reader_tdt__sev` |
| `ndr.reader.bjg` | `ndr.reader.bjg` | `ndr_reader_bjg` |
| `ndr.reader.dabrowska` | `ndr.reader.dabrowska` | `ndr_reader_dabrowska` |
| `ndr.reader.whitematter` | `ndr.reader.whitematter` | `ndr_reader_whitematter` |
| `ndr.reader.somecompany_someformat` | `ndr.reader.somecompany_someformat` | `ndr_reader_somecompany__someformat` |

## Reader Status

| Reader | getchannelsepoch | t0\_t1 | samplerate | readchannels\_epochsamples | readevents\_epochsamples\_native | read | Tests |
|---|---|---|---|---|---|---|---|
| **ndr\_reader\_intan\_\_rhd** | Yes | Yes | Yes | Yes (single-file) | Stub (empty) | Yes | 6 pass |
| **ndr\_reader\_ced\_\_smr** | Yes | Yes | Yes | Yes | Yes | Yes (via base) | 14 pass |
| **ndr\_reader\_axon\_\_abf** | Yes | Yes | Yes | Yes | Stub (empty) | Yes (via base) | 6 pass |
| **ndr\_reader\_neo** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | 24 xfail |
| **ndr\_reader\_spikegadgets\_\_rec** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | xfail |
| **ndr\_reader\_tdt\_\_sev** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
| **ndr\_reader\_bjg** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
| **ndr\_reader\_dabrowska** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |
| **ndr\_reader\_whitematter** | Stub (empty) | Stub | Stub | NotImplementedError | Stub (empty) | No | skipped |

**Legend:**
- **Yes** — Fully implemented and tested with example data
- **Stub (empty)** — Returns empty arrays / default values; no errors raised
- **NotImplementedError** — Raises an exception; not yet implemented
- **Stub** — Inherits base class default (empty list, `[[nan,nan]]`, etc.)

## Format Parsers

Low-level format parsers (under `ndr.format.*`) that read binary files:

| Format | Module | Status |
|---|---|---|
| Intan RHD | `ndr.format.intan` | Implemented (header + single-file data reader) |
| CED SMR/SON | `ndr.format.ced` | Implemented (via `neo` library) |
| Axon ABF | `ndr.format.axon` | Implemented (via `pyabf` library) |
| SpikeGadgets REC | `ndr.format.spikegadgets` | Implemented (config, analog, digital, trode) |
| TDT SEV | `ndr.format.tdt` | Implemented (header + channel reader) |
| BJG | `ndr.format.bjg` | Implemented (header + data reader) |
| Dabrowska | `ndr.format.dabrowska` | Implemented (header + data reader) |
| WhiteMatter | `ndr.format.whitematter` | Implemented (header + data reader) |
| Neo / Blackrock | `ndr.format.neo` | Implemented (utilities) |
| Binary Matrix | `ndr.format.binarymatrix` | Implemented |
| Text Signal | `ndr.format.textSignal` | Implemented |

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.

## Reader Wrapper

The top-level `ndr_reader` class (`reader_wrapper.py`) wraps any format-specific reader and adds:

| Feature | Status |
|---|---|
| `read()` convenience method | Implemented |
| `readevents_epochsamples()` with derived events (dep, den, dimp, dimn) | Implemented |
| Delegation to underlying reader | Implemented |

## External Dependencies

| Dependency | Used by | Purpose |
|---|---|---|
| `neo` | ndr\_reader\_ced\_\_smr, ndr\_reader\_neo | Read CED SMR/SON and Blackrock files |
| `pyabf` | ndr\_reader\_axon\_\_abf | Read Axon Binary Format files |
| `numpy` | All readers | Array operations |

## Test Summary

```
38 passed, 28 xfailed, 13 skipped, 2 failed (pre-existing spikegadgets format test issues)
```

Example data files are included in `src/ndr/example_data/` for Intan (.rhd), CED (.smr), Axon (.abf), SpikeGadgets (.rec), and Blackrock (.nev, .ns2).
15 changes: 15 additions & 0 deletions docs/developer_notes/PYTHON_PORTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ Function and class names must match MATLAB exactly.
- **Case Preservation:** Use `readchannels_epochsamples`, not `read_channels_epoch_samples`.
- **Directory Parity:** Python file paths must mirror MATLAB `+namespace` paths
(e.g., `+ndr/+reader` -> `src/ndr/reader/`).
- **Class Name Mirror Rule:** Python class names are derived from the fully-qualified
MATLAB class name by applying two substitutions:
1. Periods (`.`) are replaced with single underscores (`_`).
2. Existing underscores (`_`) in the MATLAB name are replaced with double
underscores (`__`).

Examples:
| MATLAB qualified name | Python class name |
|--------------------------------------|----------------------------------------|
| `ndr.reader` | `ndr_reader` |
| `ndr.reader.base` | `ndr_reader_base` |
| `ndr.reader.intan_rhd` | `ndr_reader_intan__rhd` |
| `ndr.reader.ced_smr` | `ndr_reader_ced__smr` |
| `ndr.reader.axon_abf` | `ndr_reader_axon__abf` |
| `ndr.reader.somecompany_someformat` | `ndr_reader_somecompany__someformat` |

## 3. The Porting Workflow (The Bridge Protocol)
1. **Check the Bridge:** Open the `ndr_matlab_python_bridge.yaml` in the target package.
Expand Down
2 changes: 1 addition & 1 deletion src/ndr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
A Python port of NDR-matlab (https://github.com/VH-Lab/NDR-matlab).
"""

from ndr.reader_wrapper import Reader as reader
from ndr.reader_wrapper import ndr_reader as reader

__version__ = "0.1.0"
2 changes: 2 additions & 0 deletions src/ndr/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
Port of +ndr/+data/
"""

from ndr.data.assign import assign
from ndr.data.colvec import colvec
from ndr.data.rowvec import rowvec
from ndr.data.struct2namevaluepair import struct2namevaluepair
65 changes: 65 additions & 0 deletions src/ndr/data/assign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Assign name/value pairs into a target namespace (dict).

Port of +ndr/+data/assign.m
"""

from __future__ import annotations

from typing import Any

from ndr.data.struct2namevaluepair import struct2namevaluepair


def assign(target: dict[str, Any], *args: Any) -> dict[str, Any]:
"""Apply a list of name/value pair assignments to *target*.

In MATLAB ``ndr.data.assign`` uses ``assignin('caller', ...)`` to inject
variables into the caller's workspace. In Python the idiomatic
equivalent is to update a dictionary (typically ``locals()`` or an
options dict) and return it.

Parameters
----------
target : dict
The dictionary to update with the supplied name/value pairs.
*args
Either a single ``dict`` (struct equivalent), a single ``list``
of alternating name/value items, or inline alternating
``name, value, name, value, ...`` arguments.

Returns
-------
dict
The updated *target* dictionary (same object, mutated in-place).

Examples
--------
>>> opts = {'z': 0}
>>> assign(opts, 'z', 4)
{'z': 4}

>>> assign({}, {'a': 1, 'b': 2})
{'a': 1, 'b': 2}

>>> assign({}, ['x', 10, 'y', 20])
{'x': 10, 'y': 20}
"""
# Normalise a single-argument form (dict or list) into a flat sequence
if len(args) == 1:
arg = args[0]
if isinstance(arg, dict):
flat: list[Any] = struct2namevaluepair(arg)
elif isinstance(arg, (list, tuple)):
flat = list(arg)
else:
raise TypeError("A single argument must be a dict or a list of name/value pairs.")
else:
flat = list(args)

names = flat[0::2]
values = flat[1::2]

for name, value in zip(names, values):
target[name] = value

return target
47 changes: 47 additions & 0 deletions src/ndr/data/ndr_matlab_python_bridge.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
project_metadata:
bridge_version: "1.1"
naming_policy: "Strict MATLAB Mirror"
indexing_policy: "Semantic Parity (1-based for user concepts, 0-based for internal data)"

functions:
- name: assign
matlab_path: "+ndr/+data/assign.m"
python_path: "ndr/data/assign.py"
input_arguments:
- name: target
type_python: "dict[str, Any]"
- name: args
type_python: "*Any"
output_arguments:
- name: target
type_python: "dict[str, Any]"

- name: colvec
matlab_path: "+ndr/+data/colvec.m"
python_path: "ndr/data/colvec.py"
input_arguments:
- name: x
type_python: "numpy.ndarray"
output_arguments:
- name: y
type_python: "numpy.ndarray"

- name: rowvec
matlab_path: "+ndr/+data/rowvec.m"
python_path: "ndr/data/rowvec.py"
input_arguments:
- name: x
type_python: "numpy.ndarray"
output_arguments:
- name: y
type_python: "numpy.ndarray"

- name: struct2namevaluepair
matlab_path: "+ndr/+data/struct2namevaluepair.m"
python_path: "ndr/data/struct2namevaluepair.py"
input_arguments:
- name: thestruct
type_python: "dict[str, Any]"
output_arguments:
- name: nv
type_python: "list[Any]"
41 changes: 41 additions & 0 deletions src/ndr/data/struct2namevaluepair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Convert a dict to a flat list of name/value pairs.

Port of +ndr/+data/struct2namevaluepair.m
"""

from __future__ import annotations

from typing import Any


def struct2namevaluepair(thestruct: dict[str, Any]) -> list[Any]:
"""Convert a dictionary to a flat list of name/value pairs.

This is useful for passing name/value pairs to functions that accept
them as extra keyword arguments. Each key of the dictionary is used as
the 'name', and the corresponding value is used as the 'value'.

Parameters
----------
thestruct : dict
Input dictionary mapping parameter names to values.

Returns
-------
list
Flat list alternating between keys and values,
e.g. ``['param1', 1, 'param2', 2]``.

Examples
--------
>>> struct2namevaluepair({'param1': 1, 'param2': 2})
['param1', 1, 'param2', 2]
"""
if not thestruct:
return []

nv: list[Any] = []
for key, value in thestruct.items():
nv.append(key)
nv.append(value)
return nv
Loading
Loading