From 0bc30fcc917c760a08266e31b550d236e33a9f27 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Tue, 24 Mar 2026 14:44:11 -0600 Subject: [PATCH 1/6] No nan on spikes times --- CHANGELOG.md | 1 + src/nwbinspector/checks/__init__.py | 2 ++ src/nwbinspector/checks/_ecephys.py | 11 +++++++++++ tests/unit_tests/test_ecephys.py | 25 +++++++++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5574f305..b6d679a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### New Checks * Added `check_imaging_plane_location_allen_ccf`, `check_electrodes_location_allen_ccf`, and `check_intracellular_electrode_location_allen_ccf` to validate location fields against Allen Mouse Brain CCF ontology terms when subject species is mouse. [#671](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/671) +* Added `check_spike_times_without_nans` to detect NaN values in Units spike times, which indicate conversion bugs or unclean array padding. [#689](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/689) ### Improvements diff --git a/src/nwbinspector/checks/__init__.py b/src/nwbinspector/checks/__init__.py index 266dbc33..8101a74e 100644 --- a/src/nwbinspector/checks/__init__.py +++ b/src/nwbinspector/checks/__init__.py @@ -12,6 +12,7 @@ check_electrodes_location_allen_ccf, check_negative_spike_times, check_spike_times_not_in_unobserved_interval, + check_spike_times_without_nans, check_units_table_duration, ) from ._general import ( @@ -177,6 +178,7 @@ "check_spatial_series_dims", "check_spatial_series_degrees_magnitude", "check_ascending_spike_times", + "check_spike_times_without_nans", "check_electrical_series_unscaled_data", "check_rate_is_positive", "check_imaging_plane_location_allen_ccf", diff --git a/src/nwbinspector/checks/_ecephys.py b/src/nwbinspector/checks/_ecephys.py index fd139f47..0a246d52 100644 --- a/src/nwbinspector/checks/_ecephys.py +++ b/src/nwbinspector/checks/_ecephys.py @@ -123,6 +123,17 @@ def check_spike_times_not_in_unobserved_interval(units_table: Units, nunits: int return None +@register_check(importance=Importance.CRITICAL, neurodata_type=Units) +def check_spike_times_without_nans(units_table: Units) -> Optional[InspectorMessage]: + """Check if the Units table contains NaN values in spike times.""" + if "spike_times" not in units_table: + return None + + if np.any(np.isnan(np.asarray(units_table["spike_times"].target.data[:]))): + return InspectorMessage(message="Units table contains NaN spike times. Spike times should be valid timestamps in seconds.") + return None + + @register_check(importance=Importance.BEST_PRACTICE_VIOLATION, neurodata_type=Units) def check_ascending_spike_times(units_table: Units, nelems: Optional[int] = NELEMS) -> Optional[InspectorMessage]: """ diff --git a/tests/unit_tests/test_ecephys.py b/tests/unit_tests/test_ecephys.py index d5b5c160..3c07def2 100644 --- a/tests/unit_tests/test_ecephys.py +++ b/tests/unit_tests/test_ecephys.py @@ -19,6 +19,7 @@ check_electrodes_location_allen_ccf, check_negative_spike_times, check_spike_times_not_in_unobserved_interval, + check_spike_times_without_nans, check_units_table_duration, ) @@ -290,6 +291,30 @@ def test_check_spike_times_not_in_unobserved_interval_multiple_units(): ) +class TestCheckSpikeTimesWithoutNans(TestCase): + def setUp(self): + self.units_table = Units() + + def test_spike_times_without_nans_valid(self): + self.units_table.add_unit(spike_times=[0.0, 0.1, 0.2]) + self.units_table.add_unit(spike_times=[1.0, 1.1, 1.2]) + assert check_spike_times_without_nans(units_table=self.units_table) is None + + def test_spike_times_with_nans(self): + self.units_table.add_unit(spike_times=[0.0, np.nan, 0.2]) + assert check_spike_times_without_nans(units_table=self.units_table) == InspectorMessage( + message="Units table contains NaN spike times. Spike times should be valid timestamps in seconds.", + importance=Importance.CRITICAL, + check_function_name="check_spike_times_without_nans", + object_type="Units", + object_name="Units", + location="/", + ) + + def test_spike_times_without_nans_empty(self): + assert check_spike_times_without_nans(units_table=self.units_table) is None + + class TestCheckAscendingSpikeTimes(TestCase): def setUp(self): self.units_table = Units() From f442ece3274ec46d8b074b5dfd3bf0a596167fec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:44:56 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/nwbinspector/checks/_ecephys.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nwbinspector/checks/_ecephys.py b/src/nwbinspector/checks/_ecephys.py index 0a246d52..ef3a1dcc 100644 --- a/src/nwbinspector/checks/_ecephys.py +++ b/src/nwbinspector/checks/_ecephys.py @@ -130,7 +130,9 @@ def check_spike_times_without_nans(units_table: Units) -> Optional[InspectorMess return None if np.any(np.isnan(np.asarray(units_table["spike_times"].target.data[:]))): - return InspectorMessage(message="Units table contains NaN spike times. Spike times should be valid timestamps in seconds.") + return InspectorMessage( + message="Units table contains NaN spike times. Spike times should be valid timestamps in seconds." + ) return None From b7eb85e160a37fe0c8bb7f3ef2d9f4c4b2e5f69d Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Wed, 25 Mar 2026 09:04:36 -0400 Subject: [PATCH 3/6] Add best practice docs for check_spike_times_without_nans Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/best_practices/ecephys.rst | 13 +++++++++++++ src/nwbinspector/checks/_ecephys.py | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/best_practices/ecephys.rst b/docs/best_practices/ecephys.rst index bd050620..a18accf1 100644 --- a/docs/best_practices/ecephys.rst +++ b/docs/best_practices/ecephys.rst @@ -104,6 +104,19 @@ Check function: :py:meth:`~nwbinspector.checks._ecephys.check_spike_times_not_in +.. _best_practice_spike_times_without_nans: + +No NaN Spike Times +~~~~~~~~~~~~~~~~~~ + +Spike times are physical event timestamps and must always be valid floating-point numbers. A NaN value in the +``spike_times`` column typically indicates a conversion bug or array padding that was not cleaned up before writing +to NWB. Unlike missing data in continuous signals, a spike either occurred at a specific time or it did not, so +NaN is never meaningful. Clean your spike time arrays to remove any NaN values before adding them to the Units table. + +Check function: :py:meth:`~nwbinspector.checks._ecephys.check_spike_times_without_nans` + + .. _best_practice_ascending_spike_times: Ascending Spike Times diff --git a/src/nwbinspector/checks/_ecephys.py b/src/nwbinspector/checks/_ecephys.py index ef3a1dcc..152edf07 100644 --- a/src/nwbinspector/checks/_ecephys.py +++ b/src/nwbinspector/checks/_ecephys.py @@ -125,7 +125,11 @@ def check_spike_times_not_in_unobserved_interval(units_table: Units, nunits: int @register_check(importance=Importance.CRITICAL, neurodata_type=Units) def check_spike_times_without_nans(units_table: Units) -> Optional[InspectorMessage]: - """Check if the Units table contains NaN values in spike times.""" + """ + Check if the Units table contains NaN values in spike times. + + Best Practice: :ref:`best_practice_spike_times_without_nans` + """ if "spike_times" not in units_table: return None From 60fab28ba7e76fa6a095d1d0941ae1cf20addc5e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:09:00 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/checks_by_importance.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/checks_by_importance.rst b/docs/checks_by_importance.rst index 1e41f64f..aaaba01d 100644 --- a/docs/checks_by_importance.rst +++ b/docs/checks_by_importance.rst @@ -93,4 +93,3 @@ BEST PRACTICE SUGGESTION * :py:func:`~nwbinspector.checks._tables.check_col_not_nan` * :py:func:`~nwbinspector.checks._tables.check_table_time_columns_are_not_negative` * :py:func:`~nwbinspector.checks._time_series.check_timestamp_of_the_first_sample_is_not_negative` - From 131663520148a2bd8e2157bc35ae58ac8d40d24a Mon Sep 17 00:00:00 2001 From: Ryan Ly <310197+rly@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:58:52 -0700 Subject: [PATCH 5/6] Delete docs/checks_by_importance.rst --- docs/checks_by_importance.rst | 95 ----------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 docs/checks_by_importance.rst diff --git a/docs/checks_by_importance.rst b/docs/checks_by_importance.rst deleted file mode 100644 index aaaba01d..00000000 --- a/docs/checks_by_importance.rst +++ /dev/null @@ -1,95 +0,0 @@ -Checks by Importance -====================== - -This section lists the available checks organized by their importance level. - -CRITICAL ---------- - -* :py:func:`~nwbinspector.checks._behavior.check_spatial_series_dims` -* :py:func:`~nwbinspector.checks._ecephys.check_electrical_series_dims` -* :py:func:`~nwbinspector.checks._ecephys.check_ascending_spike_times` -* :py:func:`~nwbinspector.checks._ecephys.check_units_table_duration` -* :py:func:`~nwbinspector.checks._general.check_name_slashes` -* :py:func:`~nwbinspector.checks._image_series.check_image_series_external_file_valid` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_session_start_time_future_date` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_exists` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_age` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_id_exists` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_weight` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_sex` -* :py:func:`~nwbinspector.checks._ophys.check_roi_response_series_dims` -* :py:func:`~nwbinspector.checks._tables.check_dynamic_table_region_data_validity` -* :py:func:`~nwbinspector.checks._tables.check_ids_unique` -* :py:func:`~nwbinspector.checks._tables.check_time_intervals_duration` -* :py:func:`~nwbinspector.checks._time_series.check_data_orientation` -* :py:func:`~nwbinspector.checks._time_series.check_timestamps_match_first_dimension` -* :py:func:`~nwbinspector.checks._time_series.check_rate_is_not_zero` -* :py:func:`~nwbinspector.checks._time_series.check_rate_is_positive` - -BEST PRACTICE VIOLATION ------------------------- - -* :py:func:`~nwbinspector.checks._behavior.check_compass_direction_unit` -* :py:func:`~nwbinspector.checks._behavior.check_spatial_series_radians_magnitude` -* :py:func:`~nwbinspector.checks._behavior.check_spatial_series_degrees_magnitude` -* :py:func:`~nwbinspector.checks._ecephys.check_negative_spike_times` -* :py:func:`~nwbinspector.checks._ecephys.check_electrical_series_reference_electrodes_table` -* :py:func:`~nwbinspector.checks._ecephys.check_spike_times_not_in_unobserved_interval` -* :py:func:`~nwbinspector.checks._ecephys.check_electrical_series_unscaled_data` -* :py:func:`~nwbinspector.checks._ecephys.check_electrodes_location_allen_ccf` -* :py:func:`~nwbinspector.checks._icephys.check_intracellular_electrode_cell_id_exists` -* :py:func:`~nwbinspector.checks._icephys.check_sweeptable_deprecated` -* :py:func:`~nwbinspector.checks._icephys.check_intracellular_electrode_location_allen_ccf` -* :py:func:`~nwbinspector.checks._image_series.check_image_series_external_file_relative` -* :py:func:`~nwbinspector.checks._image_series.check_image_series_data_size` -* :py:func:`~nwbinspector.checks._image_series.check_image_series_starting_frame_without_external_file` -* :py:func:`~nwbinspector.checks._images.check_order_of_images_unique` -* :py:func:`~nwbinspector.checks._images.check_order_of_images_len` -* :py:func:`~nwbinspector.checks._images.check_index_series_points_to_image` -* :py:func:`~nwbinspector.checks._nwb_containers.check_large_dataset_compression` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_species_exists` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_species_form` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_session_id_no_slashes` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_id_no_slashes` -* :py:func:`~nwbinspector.checks._ogen.check_optogenetic_stimulus_site_has_optogenetic_series` -* :py:func:`~nwbinspector.checks._ophys.check_roi_response_series_link_to_plane_segmentation` -* :py:func:`~nwbinspector.checks._ophys.check_emission_lambda_in_nm` -* :py:func:`~nwbinspector.checks._ophys.check_excitation_lambda_in_nm` -* :py:func:`~nwbinspector.checks._ophys.check_plane_segmentation_image_mask_shape_against_ref_images` -* :py:func:`~nwbinspector.checks._ophys.check_imaging_plane_location_allen_ccf` -* :py:func:`~nwbinspector.checks._tables.check_empty_table` -* :py:func:`~nwbinspector.checks._tables.check_time_interval_time_columns` -* :py:func:`~nwbinspector.checks._tables.check_time_intervals_stop_after_start` -* :py:func:`~nwbinspector.checks._tables.check_table_values_for_dict` -* :py:func:`~nwbinspector.checks._time_series.check_regular_timestamps` -* :py:func:`~nwbinspector.checks._time_series.check_timestamps_ascending` -* :py:func:`~nwbinspector.checks._time_series.check_timestamps_without_nans` -* :py:func:`~nwbinspector.checks._time_series.check_missing_unit` -* :py:func:`~nwbinspector.checks._time_series.check_resolution` -* :py:func:`~nwbinspector.checks._time_series.check_time_series_duration` -* :py:func:`~nwbinspector.checks._time_series.check_time_series_data_is_not_empty` -* :py:func:`~nwbinspector.checks._time_series.check_rate_not_below_threshold` - -BEST PRACTICE SUGGESTION -------------------------- - -* :py:func:`~nwbinspector.checks._general.check_name_colons` -* :py:func:`~nwbinspector.checks._general.check_description` -* :py:func:`~nwbinspector.checks._nwb_containers.check_small_dataset_compression` -* :py:func:`~nwbinspector.checks._nwb_containers.check_empty_string_for_optional_attribute` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_session_start_time_old_date` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_experimenter_exists` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_experimenter_form` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_experiment_description` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_institution` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_keywords` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_doi_publications` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_subject_proper_age_range` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_processing_module_name` -* :py:func:`~nwbinspector.checks._nwbfile_metadata.check_file_extension` -* :py:func:`~nwbinspector.checks._tables.check_column_binary_capability` -* :py:func:`~nwbinspector.checks._tables.check_single_row` -* :py:func:`~nwbinspector.checks._tables.check_col_not_nan` -* :py:func:`~nwbinspector.checks._tables.check_table_time_columns_are_not_negative` -* :py:func:`~nwbinspector.checks._time_series.check_timestamp_of_the_first_sample_is_not_negative` From 4e6efde6414cf00a20427eae70cd2c7ac9124f6e Mon Sep 17 00:00:00 2001 From: Ryan Ly <310197+rly@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:07:47 -0700 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548afced..96e03b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # v0.7.3 (Upcoming) ### New Checks +* Added `check_spike_times_without_nans` to detect NaN values in Units spike times, which indicate conversion bugs or unclean array padding. [#689](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/689) ### Improvements * Raised the minimum required PyNWB to `>=4.0` to track the latest PyNWB release. [#708](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/708) @@ -32,7 +33,6 @@ * Added `check_time_intervals_start_time_not_constant` to flag TimeIntervals tables where all start_time values are identical, indicating times were likely not set relative to session start. [#677](https://github.com/NeurodataWithoutBorders/nwbinspector/issues/677) * Added `check_units_resolution_is_set` to flag when the Units table has spike_times but resolution is not set to a meaningful positive float. [#686](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/686) * Added `check_spike_times_not_in_samples` to flag when spike times appear to be stored as sample indices rather than seconds, detected by all values being integer-valued with implausibly large magnitudes. -* Added `check_spike_times_without_nans` to detect NaN values in Units spike times, which indicate conversion bugs or unclean array padding. [#689](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/689) * Added `check_units_table_has_spikes` to flag Units tables that do not contain a spike_times column. [#691](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/691)