From 40664fb386cddc5c15ed5b5c6347074937c764b6 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:04:33 -0400 Subject: [PATCH 1/6] Update spike time calculations to use unique indices to account for other non-spiking units Fix bug in spike time handling by using unique indices. --- dandi/pynwb_utils.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dandi/pynwb_utils.py b/dandi/pynwb_utils.py index 2b21335ea..ec5fdd664 100644 --- a/dandi/pynwb_utils.py +++ b/dandi/pynwb_utils.py @@ -339,19 +339,16 @@ def _get_session_duration(nwb: pynwb.NWBFile) -> float | None: # Read only the first and last spike time from each unit if "spike_times" in obj.colnames and len(obj["spike_times"]): idxs = obj["spike_times"].data[:] - - # handle bug if the first unit has no spikes - if idxs[0] == 0: - idxs = idxs[1:] + spiking_idxs = np.unique(idxs) st_data = obj["spike_times"].target if len(idxs) > 1: - start = float(np.min(np.r_[st_data[0], st_data[idxs[:-1]]])) + start = float(np.min(np.r_[st_data[0], st_data[spiking_idxs[:-1]]])) else: start = float(st_data[0]) - end = float(np.max(st_data[idxs - 1])) + end = float(np.max(st_data[spiking_idxs - 1])) start_times.append(float(start)) end_times.append(float(end)) From 377eee91c1706d4ed60d2548fb3cf7f853616fa7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 04:18:27 +0000 Subject: [PATCH 2/6] Add regression test for scattered non-spiking units --- dandi/pynwb_utils.py | 7 +++++-- dandi/tests/test_metadata.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/dandi/pynwb_utils.py b/dandi/pynwb_utils.py index ec5fdd664..989a31c40 100644 --- a/dandi/pynwb_utils.py +++ b/dandi/pynwb_utils.py @@ -339,11 +339,14 @@ def _get_session_duration(nwb: pynwb.NWBFile) -> float | None: # Read only the first and last spike time from each unit if "spike_times" in obj.colnames and len(obj["spike_times"]): idxs = obj["spike_times"].data[:] - spiking_idxs = np.unique(idxs) + spiking_idxs = np.unique(idxs[idxs > 0]) + + if len(spiking_idxs) == 0: + continue st_data = obj["spike_times"].target - if len(idxs) > 1: + if len(spiking_idxs) > 1: start = float(np.min(np.r_[st_data[0], st_data[spiking_idxs[:-1]]])) else: start = float(st_data[0]) diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index 8614a6eb1..f5ea5634e 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -642,6 +642,41 @@ def test_session_duration_with_units(tmp_path: Path) -> None: assert abs(duration - 245.0) < 1.0 # Allow small floating point errors +@pytest.mark.ai_generated +def test_session_duration_with_scattered_nonspiking_units(tmp_path: Path) -> None: + """Test session duration with multiple non-spiking units in Units table.""" + nwb_path = tmp_path / "test_duration_scattered_nonspiking_units.nwb" + session_start = datetime(2020, 1, 1, 12, 0, 0, tzinfo=tzutc()) + + nwbfile = NWBFile( + session_description="test session with scattered non-spiking units", + identifier="test_scattered_nonspiking_units_123", + session_start_time=session_start, + ) + + nwbfile.add_unit(spike_times=np.array([])) + nwbfile.add_unit(spike_times=np.array([10.0, 20.0])) + nwbfile.add_unit(spike_times=np.array([])) + nwbfile.add_unit(spike_times=np.array([5.0, 250.0])) + nwbfile.add_unit(spike_times=np.array([])) + nwbfile.add_unit(spike_times=np.array([100.0])) + + with NWBHDF5IO(str(nwb_path), "w") as io: + io.write(nwbfile) + + from ..metadata.nwb import get_metadata + + metadata = get_metadata(nwb_path) + + assert "session_start_time" in metadata + assert "session_end_time" in metadata + + duration = ( + metadata["session_end_time"] - metadata["session_start_time"] + ).total_seconds() + assert abs(duration - 245.0) < 1.0 # 250s - 5s, allow floating point errors + + @pytest.mark.ai_generated def test_session_duration_with_events(tmp_path: Path) -> None: """Test that session duration includes timestamp/duration from DynamicTable""" From 9bc822c587ae2aa340d6ba0c548d4f3aea5afd3b Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:20:29 -0400 Subject: [PATCH 3/6] Remove unused metadata extraction imports Removed unused imports for metadata extraction. --- dandi/tests/test_metadata.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index f5ea5634e..84d5c0a9d 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -506,9 +506,6 @@ def test_session_duration_extraction(tmp_path: Path) -> None: with NWBHDF5IO(str(nwb_path), "w") as io: io.write(nwbfile) - # Extract metadata - from ..metadata.nwb import get_metadata, nwb2asset - metadata = get_metadata(nwb_path) # Check that session_end_time was calculated From 3cda12cde9d531f4c88278504185fd3659a21cdb Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:21:15 -0400 Subject: [PATCH 4/6] Clean up imports in test_metadata.py Removed unnecessary import statements for get_metadata in multiple test cases. --- dandi/tests/test_metadata.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index 84d5c0a9d..d8f35d311 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -564,9 +564,6 @@ def test_session_duration_with_trials(tmp_path: Path) -> None: with NWBHDF5IO(str(nwb_path), "w") as io: io.write(nwbfile) - # Extract metadata - from ..metadata.nwb import get_metadata, nwb2asset - metadata = get_metadata(nwb_path) # Check that session_end_time was calculated @@ -623,9 +620,6 @@ def test_session_duration_with_units(tmp_path: Path) -> None: with NWBHDF5IO(str(nwb_path), "w") as io: io.write(nwbfile) - # Extract metadata - from ..metadata.nwb import get_metadata - metadata = get_metadata(nwb_path) # Check that session_end_time was calculated @@ -661,8 +655,6 @@ def test_session_duration_with_scattered_nonspiking_units(tmp_path: Path) -> Non with NWBHDF5IO(str(nwb_path), "w") as io: io.write(nwbfile) - from ..metadata.nwb import get_metadata - metadata = get_metadata(nwb_path) assert "session_start_time" in metadata @@ -724,9 +716,6 @@ def test_session_duration_with_events(tmp_path: Path) -> None: with NWBHDF5IO(str(nwb_path), "w") as io: io.write(nwbfile) - # Extract metadata - from ..metadata.nwb import get_metadata - metadata = get_metadata(nwb_path) # Check that session_end_time was calculated From fab864bffb118a6e82f483a4179465578b080948 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:22:46 -0400 Subject: [PATCH 5/6] Clean up test_metadata.py by removing blank line Remove unnecessary blank line before assertions. --- dandi/tests/test_metadata.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index d8f35d311..758397465 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -656,7 +656,6 @@ def test_session_duration_with_scattered_nonspiking_units(tmp_path: Path) -> Non io.write(nwbfile) metadata = get_metadata(nwb_path) - assert "session_start_time" in metadata assert "session_end_time" in metadata From 9acfc5829028ebab2fcd25049c66aeb56c7c35dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 04:25:04 +0000 Subject: [PATCH 6/6] Add scattered non-spiking units regression test --- dandi/pynwb_utils.py | 12 +++++++----- dandi/tests/test_metadata.py | 7 +++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dandi/pynwb_utils.py b/dandi/pynwb_utils.py index 989a31c40..75a4ee4a2 100644 --- a/dandi/pynwb_utils.py +++ b/dandi/pynwb_utils.py @@ -339,19 +339,21 @@ def _get_session_duration(nwb: pynwb.NWBFile) -> float | None: # Read only the first and last spike time from each unit if "spike_times" in obj.colnames and len(obj["spike_times"]): idxs = obj["spike_times"].data[:] - spiking_idxs = np.unique(idxs[idxs > 0]) + # Keep only boundaries where cumulative spike count increases. + # Non-spiking units repeat the prior cumulative index and are skipped. + unit_end_idxs = idxs[np.diff(np.r_[0, idxs]) > 0] - if len(spiking_idxs) == 0: + if len(unit_end_idxs) == 0: continue st_data = obj["spike_times"].target - if len(spiking_idxs) > 1: - start = float(np.min(np.r_[st_data[0], st_data[spiking_idxs[:-1]]])) + if len(unit_end_idxs) > 1: + start = float(np.min(np.r_[st_data[0], st_data[unit_end_idxs[:-1]]])) else: start = float(st_data[0]) - end = float(np.max(st_data[spiking_idxs - 1])) + end = float(np.max(st_data[unit_end_idxs - 1])) start_times.append(float(start)) end_times.append(float(end)) diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index 758397465..00520d08f 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -634,7 +634,7 @@ def test_session_duration_with_units(tmp_path: Path) -> None: @pytest.mark.ai_generated -def test_session_duration_with_scattered_nonspiking_units(tmp_path: Path) -> None: +def test_session_duration_with_scattered_non_spiking_units(tmp_path: Path) -> None: """Test session duration with multiple non-spiking units in Units table.""" nwb_path = tmp_path / "test_duration_scattered_nonspiking_units.nwb" session_start = datetime(2020, 1, 1, 12, 0, 0, tzinfo=tzutc()) @@ -659,10 +659,13 @@ def test_session_duration_with_scattered_nonspiking_units(tmp_path: Path) -> Non assert "session_start_time" in metadata assert "session_end_time" in metadata + end_offset = (metadata["session_end_time"] - session_start).total_seconds() + assert abs(end_offset - 245.0) < 1.0 + duration = ( metadata["session_end_time"] - metadata["session_start_time"] ).total_seconds() - assert abs(duration - 245.0) < 1.0 # 250s - 5s, allow floating point errors + assert abs(duration - 245.0) < 1.0 # max 250s and min 5s spike times @pytest.mark.ai_generated