diff --git a/src/dynamic_foraging_processing/pipeline/_pipeline.py b/src/dynamic_foraging_processing/pipeline/_pipeline.py index 2a08af9..00390f5 100644 --- a/src/dynamic_foraging_processing/pipeline/_pipeline.py +++ b/src/dynamic_foraging_processing/pipeline/_pipeline.py @@ -59,10 +59,11 @@ #: ``TimeIntervals`` requires both, so they are derived here. _NWB_START_COLUMN = "quiescent_start_time" -#: Columns NWB's required native ``stop_time`` is taken from, in order of -#: preference: the end of the ITI, falling back to its start on the last trial -#: of the session (where the ITI end is unknown). -_NWB_STOP_COLUMNS = ("ITI_stop_time", "ITI_start_time") +#: Trials-table column NWB's required native ``stop_time`` is taken from: the +#: end of the ITI. This is ``NaN`` on the last trial of the session (whose ITI +#: end is unknown) and the ``NaN`` is propagated rather than substituted, so an +#: unknown trial end reads as unknown instead of as a shortened trial. +_NWB_STOP_COLUMN = "ITI_stop_time" #: Source repository recorded in the ``processing.json`` data process. _CODE_URL = "https://github.com/AllenNeuralDynamics/dynamic-foraging-processing" @@ -236,8 +237,9 @@ def _trial_extent(row: pd.Series) -> t.Tuple[float, float]: The trials table has no trial start/stop columns of its own, so the trial's extent is taken from its period bounds: it starts with the - quiescent period and ends with the ITI, falling back to the ITI start on - the last trial of the session (whose ITI end is unknown). + quiescent period and ends with the ITI. The last trial of the session has + no ITI end, so its stop time is ``NaN`` — an unknown end is reported as + unknown rather than substituted with an earlier landmark. Parameters ---------- @@ -247,11 +249,10 @@ def _trial_extent(row: pd.Series) -> t.Tuple[float, float]: Returns ------- tuple of (float, float) - The trial start and stop time (seconds). + The trial start and stop time (seconds); the stop time is ``NaN`` + where the ITI end is unknown. """ - stops = [row[column] for column in _NWB_STOP_COLUMNS if pd.notnull(row[column])] - stop = stops[0] if stops else np.nan - return float(row[_NWB_START_COLUMN]), float(stop) + return float(row[_NWB_START_COLUMN]), float(row[_NWB_STOP_COLUMN]) @classmethod def _add_trials(cls, nwb_file: pynwb.NWBFile, trials: pd.DataFrame) -> None: @@ -265,7 +266,7 @@ def _add_trials(cls, nwb_file: pynwb.NWBFile, trials: pd.DataFrame) -> None: (named ``id``) is replicated as each trial's NWB ``id``. An empty table (or one missing the period columns the extent is derived from) is skipped. """ - required = (_NWB_START_COLUMN, *_NWB_STOP_COLUMNS) + required = (_NWB_START_COLUMN, _NWB_STOP_COLUMN) if trials.empty or any(col not in trials.columns for col in required): return descriptions = TrialConfig.column_descriptions() diff --git a/tests/test_pipeline/test_pipeline.py b/tests/test_pipeline/test_pipeline.py index c897ea7..d7650d3 100644 --- a/tests/test_pipeline/test_pipeline.py +++ b/tests/test_pipeline/test_pipeline.py @@ -297,7 +297,7 @@ def test_add_trials_derives_native_start_and_stop_from_periods(): """NWB's native trial extent spans the quiescent start to the ITI end. The last trial has no ITI end (no following quiescent period), so its stop - time falls back to the ITI start. + time stays ``NaN`` rather than falling back to an earlier landmark. """ nwb_file = MagicMock() @@ -307,7 +307,8 @@ def test_add_trials_derives_native_start_and_stop_from_periods(): (call.kwargs["start_time"], call.kwargs["stop_time"]) for call in nwb_file.add_trial.call_args_list ] - assert extents == [(0.0, 1.0), (1.0, 1.4)] + assert extents[0] == (0.0, 1.0) + assert extents[1][0] == 1.0 and np.isnan(extents[1][1]) def test_add_trials_skips_frame_without_period_columns():