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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **`vhneuropixelsGLX_nidq` vhlab DAQ system** (mirroring VH-Lab/NDI-matlab#1127).
Reads the SpikeGLX NI-DAQ stream (`*.nidq.bin` / `*.nidq.meta`) with the NDR
`neuropixelsGLX` reader, so probes on NI-DAQ digital lines can be described
in `epochprobemap.txt` -- e.g. an optogenetic fiber on XD4 as
`vhneuropixelsGLX_nidq:dimp5` (SpikeGLX XDn is channel di(n+1)).
`vhneuropixelsGLX` reads the IMEC `.ap.bin`, whose digital lines are the IMEC
sync bits. A blank vhlab session now has ten DAQ systems.

- **Update button: a package table with tick boxes** (mirroring
NDI-matlab 19b8ab573 and 749b1a266). The navigator's Update button now lists
NDI-python and each dependency clone in `~/.ndi/tools` in
Expand Down Expand Up @@ -197,6 +205,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Fixed

- **Digital-edge events were one sample early.** `ndi_daq_reader_mfdaq`'s
`dep`, `den`, `dimp` and `dimn` events (the non-ingested read) timed each
"on" edge at the last sample before the transition; MATLAB, the "off" edges
and the ingested read all use the first sample after it. A line going high
at 1.000 s at 1 kHz gave 0.999 s. They now agree.

- **Reading samples through a DAQ system: raw and ingested epochs now agree.**
`ndi_daq_system_mfdaq.readchannels_epochsamples` takes 0-based samples, as
`epochtimes2samples` and `epochsamples2times` do, but passed them unchanged
Expand Down
4 changes: 2 additions & 2 deletions src/ndi/daq/mfdaq.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,13 @@ def _read_derived_events(

# Find transitions
if ct in ("dep", "dimp"): # positive transitions
on_samples = np.where((di_data[:-1] == 0) & (di_data[1:] == 1))[0]
on_samples = 1 + np.where((di_data[:-1] == 0) & (di_data[1:] == 1))[0]
if ct == "dimp":
off_samples = 1 + np.where((di_data[:-1] == 1) & (di_data[1:] == 0))[0]
else:
off_samples = np.array([], dtype=int)
else: # negative transitions (den, dimn)
on_samples = np.where((di_data[:-1] == 1) & (di_data[1:] == 0))[0]
on_samples = 1 + np.where((di_data[:-1] == 1) & (di_data[1:] == 0))[0]
if ct == "dimn":
off_samples = 1 + np.where((di_data[:-1] == 0) & (di_data[1:] == 1))[0]
else:
Expand Down
12 changes: 11 additions & 1 deletion src/ndi/daq/ndi_matlab_python_bridge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ classes:
- name: mfdaq_reader
type: class
matlab_path: "+ndi/+daq/+reader/mfdaq.m"
matlab_last_sync_hash: "5c5784c6a"
matlab_last_sync_hash: "dfba35572"
decision_log: >
NDI-matlab 770c1eb3 clamps readchannels_epochsamples_ingested's
window to what the epoch has: a finite s0 below the first sample
Expand Down Expand Up @@ -427,6 +427,16 @@ classes:
epoch's segments over the workers. Ported, with the same plan fields
and weights; ingest_epochfiles is now plan, each segment, attach, as
in MATLAB. 5c5784c6a only marks a growing array for Code Analyzer.

VH-Lab/NDI-matlab#1127 (d37bf16c7) fixes readevents_epochsamples and
readevents_epochsamples_ingested so dimn returns its off (0->1)
transitions; the dimn branch tested 'dimp'. Nothing to port for that:
this port's dimn branches were already right. Checking them found a
Python-only bug instead: the non-ingested read timed each dep/den/
dimp/dimn "on" edge one sample early (np.where without the + 1 that
MATLAB's 1 + find(...) and the "off" edges have). Fixed here.
Synced to dfba35572, the head of #1127's branch; the commits after
5c5784c6a touching this file are d37bf16c7 alone.
python_path: "ndi/daq/mfdaq.py"
python_class: "ndi_daq_reader_mfdaq"
inherits: "ndi.daq.reader"
Expand Down
16 changes: 16 additions & 0 deletions src/ndi/ndi_common/daq_systems/vhlab/vhneuropixelsGLX_nidq.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Name": "vhneuropixelsGLX_nidq",
"DaqSystemClass": "ndi.daq.system.mfdaq",
"DaqReaderClass": "ndi.daq.reader.mfdaq.ndr",
"MetadataReaderClass": [],
"EpochProbeMapClass": "ndi.epoch.epochprobemap_daqsystem",
"FileParameters": [
"#.nidq\\.bin\\>",
"#.nidq\\.meta\\>",
"epochprobemap.txt"
],
"DaqReaderFileParameters": "neuropixelsGLX",
"MetadataReaderFileParameters": [],
"EpochProbeMapFileParameters": "epochprobemap.txt",
"HasEpochDirectories": true
}
9 changes: 8 additions & 1 deletion src/ndi/ndi_matlab_python_bridge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,7 @@ not_applicable:
# =========================================================================
- name: setup
matlab_path: "+ndi/+setup/"
matlab_last_sync_hash: "fd7ddbf06"
matlab_last_sync_hash: "dfba35572"
python_path: "ndi/setup/"
status: ported
decision_log: >
Expand Down Expand Up @@ -1362,6 +1362,13 @@ not_applicable:
imports legacy lab data), so nothing here follows; lab, labNames and
the per-lab wrappers are unchanged.

Re-examined against NDI-matlab dfba35572 (VH-Lab/NDI-matlab#1127). Its
only net change under +ndi/+setup/ is a comment in vhlab.m describing
the new vhneuropixelsGLX_nidq DAQ system (0b3c7d3dd also added it to
+daq/+system/export_vhlab_daq_systems.m, and dfba35572 took that back
out). No code moved, so nothing follows here; the DAQ system itself is
ndi_common/daq_systems/vhlab/vhneuropixelsGLX_nidq.json, copied as is.

# =========================================================================
# ndi.docs namespace — MATLAB documentation generation
# =========================================================================
Expand Down
70 changes: 70 additions & 0 deletions tests/test_daq_derived_digital_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Events derived from a digital channel: dep, den, dimp, dimn.

Mirrors NDI-matlab tests/+ndi/+unittest/+daq/+reader/DigitalMarkEventsTest.m.
An event is timed at the first sample *after* the transition, as in
MATLAB's ``1 + find(d(1:end-1)==0 & d(2:end)==1)``: for a line that goes
high at t = 1.0 s, the rising edge is at 1.0 s, not one sample earlier.
"""

from __future__ import annotations

import numpy as np
import pytest

from ndi.daq.mfdaq import ChannelInfo, ndi_daq_reader_mfdaq

SR = 1000.0
DURATION = 10.0


class _PulseReader(ndi_daq_reader_mfdaq):
"""di1 is 1 for 1.0 <= t < 1.1 and 3.0 <= t < 3.1, else 0 (1 kHz, 0..10 s)."""

def t0_t1(self, epochfiles):
return [(0.0, DURATION)]

def getchannelsepoch(self, epochfiles):
return [ChannelInfo(name="di1", type="digital_in", time_channel=1)]

def samplerate(self, epochfiles, channeltype, channel):
n = 1 if isinstance(channel, int) else len(channel)
return np.full(n, SR)

def readchannels_epochsamples(self, channeltype, channel, epochfiles, s0, s1):
# s0/s1 come from epochtimes2samples, which counts from t0 = 0.
t = np.arange(s0, s1 + 1) / SR
ct = channeltype if isinstance(channeltype, str) else channeltype[0]
if ct in ("time", "t"):
return t.reshape(-1, 1)
d = (((t >= 1.0) & (t < 1.1)) | ((t >= 3.0) & (t < 3.1))).astype(float)
return d.reshape(-1, 1)


@pytest.fixture
def reader():
return _PulseReader()


def test_dimp_on_off(reader):
t, d = reader.readevents_epochsamples("dimp", 1, ["mock"], 0.0, DURATION)
np.testing.assert_allclose(t, [1.0, 1.1, 3.0, 3.1], atol=1e-9)
np.testing.assert_array_equal(d, [1, -1, 1, -1])


def test_dimn_on_off(reader):
# dimn: "on" is the 1->0 transition and "off" is the 0->1 transition.
t, d = reader.readevents_epochsamples("dimn", 1, ["mock"], 0.0, DURATION)
np.testing.assert_allclose(t, [1.0, 1.1, 3.0, 3.1], atol=1e-9)
np.testing.assert_array_equal(d, [-1, 1, -1, 1])


def test_dep_rising_edges_only(reader):
t, d = reader.readevents_epochsamples("dep", 1, ["mock"], 0.0, DURATION)
np.testing.assert_allclose(t, [1.0, 3.0], atol=1e-9)
np.testing.assert_array_equal(d, [1, 1])


def test_den_falling_edges_only(reader):
t, d = reader.readevents_epochsamples("den", 1, ["mock"], 0.0, DURATION)
np.testing.assert_allclose(t, [1.1, 3.1], atol=1e-9)
np.testing.assert_array_equal(d, [1, 1])
43 changes: 43 additions & 0 deletions tests/test_vhneuropixelsglx_nidq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""The vhlab vhneuropixelsGLX_nidq DAQ system (SpikeGLX NI-DAQ stream).

Mirrors NDI-matlab's ndi_common/daq_systems/vhlab/vhneuropixelsGLX_nidq.json
(VH-Lab/NDI-matlab#1127). vhneuropixelsGLX reads the IMEC .ap.bin, whose
digital lines are the IMEC SY bits; this system reads the .nidq.bin, so the
NI-DAQ lines are reachable: SpikeGLX XDn is channel di(n+1).
"""

from __future__ import annotations

import tempfile

import pytest

from ndi.daq.reader.mfdaq.ndr import ndi_daq_reader_mfdaq_ndr
from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq
from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir
from ndi.session.dir import ndi_session_dir
from ndi.setup.lab import lab as setup_lab


@pytest.fixture
def nidq_system():
s = ndi_session_dir("vhtest", tempfile.mkdtemp())
setup_lab(s, "vhlab")
return s.daqsystem_load(name="vhneuropixelsGLX_nidq")


def test_reads_the_nidq_stream_with_the_ndr_glx_reader(nidq_system):
assert isinstance(nidq_system, ndi_daq_system_mfdaq)
assert isinstance(nidq_system.daqreader, ndi_daq_reader_mfdaq_ndr)
assert nidq_system.daqreader.ndr_reader_string == "neuropixelsGLX"


def test_epochs_are_nidq_files_plus_epochprobemap(nidq_system):
fn = nidq_system.filenavigator
assert isinstance(fn, ndi_file_navigator_epochdir)
assert fn.fileparameters["filematch"] == [
"#.nidq\\.bin\\>",
"#.nidq\\.meta\\>",
"epochprobemap.txt",
]
assert fn.epochprobemap_fileparameters["filematch"] == ["epochprobemap.txt"]
7 changes: 5 additions & 2 deletions tests/test_vhprairieview.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ def vhlab_session(self):
setup_lab(s, "vhlab")
return s

def test_nine_daq_systems_including_vhprairieview(self, vhlab_session):
def test_ten_daq_systems_including_vhprairieview(self, vhlab_session):
# Nine until vhneuropixelsGLX_nidq (the SpikeGLX NI-DAQ stream) joined
# vhlab, as it did in NDI-matlab (VH-Lab/NDI-matlab#1127).
summary = sessionSummary(vhlab_session)
assert len(summary["daqSystemNames"]) == 9
assert len(summary["daqSystemNames"]) == 10
assert "vhprairieview" in summary["daqSystemNames"]
assert "vhneuropixelsGLX_nidq" in summary["daqSystemNames"]

def test_vhprairieview_uses_the_image_classes(self, vhlab_session):
"""Not just present -- present with the right navigator and reader."""
Expand Down
Loading