Skip to content

Add analog event type support with threshold-based detection#54

Merged
stevevanhooser merged 11 commits intomainfrom
claude/port-ndi-matlab-changes-9ttly
Apr 13, 2026
Merged

Add analog event type support with threshold-based detection#54
stevevanhooser merged 11 commits intomainfrom
claude/port-ndi-matlab-changes-9ttly

Conversation

@stevevanhooser
Copy link
Copy Markdown
Contributor

Summary

This PR adds support for analog event channel types (aep, aen, aimp, aimn) that derive events from analog input channels by detecting threshold crossings. It includes threshold parsing, event detection logic, and infrastructure updates.

Key Changes

Core Functionality

  • Analog event type detection: Added is_analog_event_type() static method to identify and parse analog event channel types with optional threshold suffixes (e.g., aep_t2.5)
  • Threshold-based event detection: Implemented _read_analog_events() and _read_analog_events_ingested() methods that:
    • Detect upward (aep/aimp) and downward (aen/aimn) threshold crossings
    • Generate event timestamps and signed data (+1/-1 for on/off transitions)
    • Support pulse detection (aimp/aimn) with both on and off events
  • Event routing: Updated readevents_epochsamples() and readevents_epochsamples_ingested() to route analog event types to the new detection methods

Utility Functions

  • Channel type parsing: Added parse_analog_event_channeltype() to extract base type and threshold from strings like aep_t2.5
  • Device string building: Added channeltype2str() to reconstruct device string segments with threshold suffixes in correct position
  • Infinite value handling: Enhanced epochtimes2samples() and epochtimes2samples_ingested() to clamp positive infinity to the last sample of the epoch (previously only handled negative infinity)

Configuration

  • Added four new DAQ system configuration files for Neuropixels-based setups:
    • nielsen_neuropixelsGLX.json
    • vhneuropixelsGLX.json
    • vhneuropixels.json
    • vhajbpod_np.json
    • nielsen_visnp.json

Documentation

  • Updated MATLAB-Python bridge YAML with new method signatures and sync hashes
  • Marked ndi.database.fun.copy_session_to_dataset as deprecated (moved to ndi.dataset.copySessionToDataset)

Implementation Details

  • Threshold detection uses numpy boolean indexing to find sample indices where ai_data[:-1] < thresh and ai_data[1:] >= thresh (or inverse for negative crossings)
  • Events are sorted by timestamp when both on and off samples exist (pulse modes)
  • Single-channel queries return unwrapped arrays; multi-channel return lists of arrays
  • Both native and ingested data paths are supported with identical detection logic

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ

claude added 11 commits April 13, 2026 01:34
…X configs

- Add analog event channel types (aep/aen/aimp/aimn) with threshold
  suffix support to daqsystemstring.py and mfdaq.py, mirroring MATLAB
  readevents_epochsamples for both live and ingested data paths
- Add +Inf clamping to epochtimes2samples and epochtimes2samples_ingested
  so [-Inf, +Inf] maps to full epoch sample range
- Add channeltype2str and parse_analog_event_channeltype static methods
  to daqsystemstring
- Copy 5 new Neuropixels GLX DAQ system configs from NDI-matlab
- Update bridge YAMLs with new sync hashes, methods, and decision logs
  for daqsystemstring (2157c70), mfdaq (9e11fbb), dataset (f485088)
- Mark copy_session_to_dataset as deprecated in database_fun bridge
  (moved to ndi.dataset.copySessionToDataset in MATLAB)

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The nielsen_visnp DAQ system config references reader class
ndi.setup.daq.reader.mfdaq.stimulus.nielsenvisneuropixelsglx which
did not exist in Python, causing the kjnielsenlab blank session test
to show 3 DAQ systems instead of 4.

Creates a minimal stimulus reader class inheriting from the NDR reader,
following the same pattern as nielsenvisintan. Registers it in the
class registry so DAQ system loading succeeds.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The vhajbpod_np config specifies a custom FileNavigatorClass
(ndi.setup.file.navigator.vhlab_np_epochdir) which Python's lab.py
was ignoring, always defaulting to ndi.file.navigator.epochdir.

When loading MATLAB-generated artifacts, the DAQ system document
contained the custom navigator class name, which Python couldn't
resolve, causing the entire DAQ system to be silently dropped
(7 vs 8 in the symmetry test).

- Update lab.py to use FileNavigatorClass from JSON config when present
- Register vhlab_np_epochdir in class_registry mapping to epochdir

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The ndr.py wrapper was incorrectly delegating all read operations to
the SpikeInterface adapter, which defaults all channels to "analog_in"
regardless of actual type. This meant digital_in, time, and other
channel types were never properly reported.

NDR-python already has fully implemented format-specific readers (e.g.
ndr_reader_neuropixelsGLX) that correctly return channel types — but
they were never called.

Rewritten to match MATLAB's ndr.m pattern:
- Store ndr_reader_string from document or constructor
- Delegate to ndr.reader(ndr_reader_string) for all methods
- Convert NDR dict output to ChannelInfo objects
- Handle MightHaveTimeGaps for time-gap interpolation
- Convert NDR ClockType to NDI ndi_time_clocktype

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
- Run black on ndr.py to fix formatting (line-length = 100)
- Add CI lint & test commands section to AGENTS.md so agents know
  to run black, ruff, and pytest before pushing

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The ndr.py rewrite broke two symmetry tests where daqreader_ndr
documents had a default ndr_reader_string ("RHD") that didn't match
the actual data format (Axon ABF / Intan via SpikeInterface).

Also fixed a wrong relative import: 'from ...time' resolved to
'ndi.daq.time' (nonexistent) instead of 'ndi.time'. Changed to
absolute import 'from ndi.time'.

Each delegation method now tries NDR first, then falls back to
SpikeInterface on failure. This preserves the new NDR-native path
for configs that set ndr_reader_string correctly, while maintaining
backward compatibility with existing documents.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The Axon NDR symmetry test creates a daqreader_ndr document with the
default ndr_reader_string ("RHD") but Axon .abf data. CI doesn't have
spikeinterface installed, so the SpikeInterface fallback also fails.

Instead of relying on SpikeInterface as fallback, _get_ndr_reader now
accepts epochfiles and auto-detects the correct NDR reader from file
extensions (.abf -> axon_abf, .rhd -> intan_rhd, etc.) when the
stored reader string doesn't match. This removes the SpikeInterface
dependency for the common case and lets NDR handle all formats natively.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
ndr.reader("RHD") succeeds (valid alias for intan_rhd) but then
fails when asked to read .abf files. The previous fallback approach
only caught the ndr.reader() instantiation failure, not the later
read failure.

Fix: when epochfiles are available, always detect the reader from
file extensions first. This correctly maps .abf -> axon_abf before
the wrong default reader is ever used. Falls back to the stored
ndr_reader_string only when no epoch files are given or no extension
matches.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The Axon NDR symmetry test was missing the ndr_reader_string, relying
on the template default "RHD". Fixed the test to explicitly set
"axon_abf". Removed the file-extension auto-detection logic from
ndr.py — the reader type should always be set explicitly when the
DAQ system is created.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
The ndr.py rewrite now routes Axon ABF files through NDR's native
axon_abf reader instead of SpikeInterface. NDR's axon_abf reader
requires pyabf, which is in NDR's optional [formats] extra. Updated
the NDR dependency to install with this extra.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
When a custom navigator class (e.g. vhlab_np_epochdir) is mapped to a
generic Python class (epochdir), the session summary was reporting the
Python class constant instead of the document's actual class name. This
caused a mismatch with MATLAB artifacts.

After constructing the navigator from a document, override the instance's
NDI_FILENAVIGATOR_CLASS to match the document value when they differ.

https://claude.ai/code/session_01WSbupNz1kLD3RqBMg85ZjJ
@stevevanhooser stevevanhooser merged commit 4cd4f97 into main Apr 13, 2026
5 checks passed
@stevevanhooser stevevanhooser deleted the claude/port-ndi-matlab-changes-9ttly branch April 13, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants