Repository: aeye-lab/pymovements
Pull request #656 (2024-01-11 19:43:28)

fix: don't fail in event fill if event onsets/offsets not within timesteps
## Description

The fill method raised an exception if there were events with onsets/offsets outside the timestep space passed via `time`.

## Implemented changes

- [x] handle events with onset after last timestep (ignore and continue)
- [x] handle events with onset before first timestep (use first timestep as start)
- [x] handle events with offset before first timestep (ignore and continue)
- [x] handle events with onset before last timestep (use last timestep as end)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- [x] added several tests with events exceeding timestep boundaries

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/656?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`4fd1b18`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/4fd1b18320bd401495779307e5895a80f9cf1502?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`420cb27`)](https://app.codecov.io/gh/aeye-lab/pymovements/pull/656?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #656   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           54        54           
  Lines         2356      2356           
  Branches       588       588           
=========================================
  Hits          2356      2356           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/656?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).


Patch:
diff --git a/src/pymovements/events/detection/_fill.py b/src/pymovements/events/detection/_fill.py
index 2d90cf3..58b6519 100644
--- a/src/pymovements/events/detection/_fill.py
+++ b/src/pymovements/events/detection/_fill.py
@@ -60,8 +60,21 @@ def fill(
     events_mask = np.zeros(len(timesteps), dtype=bool)
 
     for row in events.frame.iter_rows(named=True):
-        idx_onset = np.where(timesteps == row['onset'])[0][0]
-        idx_offset = np.where(timesteps == row['offset'] - 1)[0][0]
+        if row['onset'] > np.max(timesteps):  # event onset after last timestep
+            continue
+
+        if row['offset'] - 1 < np.min(timesteps):  # event offset before first timestep
+            continue
+
+        if row['onset'] < np.min(timesteps):  # event onset before first timestep
+            idx_onset = 0
+        else:
+            idx_onset = np.where(timesteps == row['onset'])[0][0]
+
+        if row['offset'] > np.max(timesteps):  # event offset after last timestep
+            idx_offset = len(timesteps) - 1
+        else:
+            idx_offset = np.where(timesteps == row['offset'] - 1)[0][0]
 
         events_mask[idx_onset:idx_offset + 1] = True
 


Test patch:
diff --git a/tests/unit/gaze/detect_test.py b/tests/unit/gaze/detect_test.py
index 6dff0dc..b19500b 100644
--- a/tests/unit/gaze/detect_test.py
+++ b/tests/unit/gaze/detect_test.py
@@ -716,6 +716,57 @@ from pymovements.synthetic import step_function
             ),
             id='fill_fixation_10_ms_break_then_saccade_until_end_single_fill',
         ),
+
+        pytest.param(
+            'fill',
+            {},
+            pm.gaze.from_numpy(
+                time=np.arange(100, 200),
+                events=pm.EventDataFrame(
+                    name=['fixation', 'saccade'], onsets=[0, 50], offsets=[40, 100],
+                ),
+            ),
+            pm.EventDataFrame(
+                name=['fixation', 'saccade', 'unclassified'],
+                onsets=[0, 50, 100],
+                offsets=[40, 100, 199],
+            ),
+            id='fill_fixation_events_before_timesteps',
+        ),
+
+        pytest.param(
+            'fill',
+            {},
+            pm.gaze.from_numpy(
+                time=np.arange(0, 200),
+                events=pm.EventDataFrame(
+                    name=['fixation', 'saccade'], onsets=[210, 250], offsets=[240, 300],
+                ),
+            ),
+            pm.EventDataFrame(
+                name=['fixation', 'saccade', 'unclassified'],
+                onsets=[210, 250, 0],
+                offsets=[240, 300, 199],
+            ),
+            id='fill_fixation_events_after_timesteps',
+        ),
+
+        pytest.param(
+            'fill',
+            {},
+            pm.gaze.from_numpy(
+                time=np.arange(100, 200),
+                events=pm.EventDataFrame(
+                    name=['fixation', 'fixation'], onsets=[0, 120], offsets=[110, 220],
+                ),
+            ),
+            pm.EventDataFrame(
+                name=['fixation', 'fixation', 'unclassified'],
+                onsets=[0, 120, 110],
+                offsets=[110, 220, 119],
+            ),
+            id='fill_fixation_events_exceed_time_boundaries',
+        ),
     ],
 )
 def test_gaze_detect(method, kwargs, gaze, expected):