Repository: aeye-lab/pymovements (python)

Issue:
Add name argument to event detection functions
## Description of the problem
Currently all event detection functions simply use `fixation` or `saccade` as the name of an event.
But imagine we want to run both `idt()` and `ivt()` and compare the results.
It's not obvious how to do that easily.

## Description of a solution
A solution would be to specify the name of the events in the resulting dataframe.
This way we could say for example that `ivt` events should have the name `fixation.ivt` and `fixation.idt` for the `idt` algorithm.

## Minimum acceptance criteria
- [x] add 'name' argument to all event detection methods and the `EventDetectionCallable` protocol


Pull request:
feat(events): Add support for custom event names
## Description

This makes it possible to specify custom event names for your detected events like this:

```python
dataset.detect_events('idt', name='fixation.idt')
dataset.detect_events('ivt', name='fixation.ivt')
```

fixes #321

## Implemented changes

- [x] Added `name` parameter to event detection functions
- [x] Updated event detection tutorial to show this feature

## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] This change is or requires a documentation update

## How Has This Been Tested?

- [x] Added test cases to each event detection function

## 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


Patch:
diff --git a/docs/source/tutorials/detecting-events.ipynb b/docs/source/tutorials/detecting-events.ipynb
index 5c727f1..f092337 100644
--- a/docs/source/tutorials/detecting-events.ipynb
+++ b/docs/source/tutorials/detecting-events.ipynb
@@ -154,13 +154,23 @@
     "To be able to differentiate between the detected events, we specify custom event names for each algorithm:"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "32d700e9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset.detect_events('idt', dispersion_threshold=2.7, name='fixation.idt')\n",
+    "dataset.detect_events('ivt', velocity_threshold=20, name='fixation.ivt')"
+   ]
+  },
   {
    "cell_type": "markdown",
-   "id": "a65bb9dc",
+   "id": "acdfbf60",
    "metadata": {},
    "source": [
-    "dataset.detect_events('idt', dispersion_threshold=2.7)\n",
-    "dataset.detect_events('ivt', velocity_threshold=20)"
+    "This has added new rows with the fixation events to the event dataframe."
    ]
   },
   {
@@ -170,15 +180,17 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset.events[0].frame.filter(pl.col('name') == 'fixation').head()"
+    "dataset.events[0].frame.filter(pl.col('name') == 'fixation.idt').head()"
    ]
   },
   {
-   "cell_type": "markdown",
-   "id": "acdfbf60",
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5ebab23c",
    "metadata": {},
+   "outputs": [],
    "source": [
-    "This has added new rows with the fixation events to the event dataframe."
+    "dataset.events[0].frame.filter(pl.col('name') == 'fixation.ivt').head()"
    ]
   },
   {
diff --git a/src/pymovements/events/__init__.py b/src/pymovements/events/__init__.py
index 78e8738..026447a 100644
--- a/src/pymovements/events/__init__.py
+++ b/src/pymovements/events/__init__.py
@@ -68,11 +68,11 @@ from pymovements.events.events import EventDataFrame
 from pymovements.events.events import register_event_detection
 
 __all__ = [
-    'microsaccades',
-    'idt',
-    'ivt',
+    'EventDataFrame',
     'EventGazeProcessor',
     'EventProcessor',
-    'EventDataFrame',
+    'idt',
+    'ivt',
+    'microsaccades',
     'register_event_detection',
 ]
diff --git a/src/pymovements/events/detection/idt.py b/src/pymovements/events/detection/idt.py
index 0e711d9..81c5d48 100644
--- a/src/pymovements/events/detection/idt.py
+++ b/src/pymovements/events/detection/idt.py
@@ -59,6 +59,7 @@ def idt(
         minimum_duration: int = 100,
         dispersion_threshold: float = 1.0,
         include_nan: bool = False,
+        name: str = 'fixation',
 ) -> EventDataFrame:
     """
     Fixation identification based on dispersion threshold.
@@ -88,6 +89,8 @@ def idt(
         Threshold for dispersion for a group of consecutive samples to be identified as fixation
     include_nan: bool
         Indicator, whether we want to split events on missing/corrupt value (np.nan)
+    name:
+        Name for detected events in EventDataFrame.
 
     Returns
     -------
@@ -210,5 +213,5 @@ def idt(
     onsets_arr = np.array(onsets).flatten()
     offsets_arr = np.array(offsets).flatten()
 
-    event_df = EventDataFrame(name='fixation', onsets=onsets_arr, offsets=offsets_arr)
+    event_df = EventDataFrame(name=name, onsets=onsets_arr, offsets=offsets_arr)
     return event_df
diff --git a/src/pymovements/events/detection/ivt.py b/src/pymovements/events/detection/ivt.py
index 0a6d395..736bbb1 100644
--- a/src/pymovements/events/detection/ivt.py
+++ b/src/pymovements/events/detection/ivt.py
@@ -40,6 +40,7 @@ def ivt(
         minimum_duration: int = 100,
         velocity_threshold: float = 20.0,
         include_nan: bool = False,
+        name: str = 'fixation',
 ) -> EventDataFrame:
     """
     Identification of fixations based on velocity-threshold
@@ -68,6 +69,8 @@ def ivt(
         velocity is below the threshold, the point is classified as a fixation.
     include_nan: bool
         Indicator, whether we want to split events on missing/corrupt value (np.nan)
+    name:
+        Name for detected events in EventDataFrame.
 
     Returns
     -------
@@ -127,5 +130,5 @@ def ivt(
     offsets = timesteps[[candidate_indices[-1] for candidate_indices in candidates]].flatten()
 
     # Create event dataframe from onsets and offsets.
-    event_df = EventDataFrame(name='fixation', onsets=onsets, offsets=offsets)
+    event_df = EventDataFrame(name=name, onsets=onsets, offsets=offsets)
     return event_df
diff --git a/src/pymovements/events/detection/microsaccades.py b/src/pymovements/events/detection/microsaccades.py
index 946fa51..85e005b 100644
--- a/src/pymovements/events/detection/microsaccades.py
+++ b/src/pymovements/events/detection/microsaccades.py
@@ -43,6 +43,7 @@ def microsaccades(
         threshold_factor: float = 6,
         minimum_threshold: float = 1e-10,
         include_nan: bool = False,
+        name: str = 'saccade',
 ) -> EventDataFrame:
     """Detect micro-saccades from velocity gaze sequence.
 
@@ -76,6 +77,8 @@ def microsaccades(
         Default: 1e-10
     include_nan: bool
         Indicator, whether we want to split events on missing/corrupt value (np.nan)
+    name:
+        Name for detected events in EventDataFrame.
 
     Returns
     -------
@@ -144,7 +147,7 @@ def microsaccades(
     offsets = timesteps[[candidate_indices[-1] for candidate_indices in candidates]].flatten()
 
     # Create event dataframe from onsets and offsets.
-    event_df = EventDataFrame(name='saccade', onsets=onsets, offsets=offsets)
+    event_df = EventDataFrame(name=name, onsets=onsets, offsets=offsets)
     return event_df
 
 


Test patch:
diff --git a/tests/events/detection/idt_test.py b/tests/events/detection/idt_test.py
index 1c63d78..b3e9901 100644
--- a/tests/events/detection/idt_test.py
+++ b/tests/events/detection/idt_test.py
@@ -151,6 +151,20 @@ def test_idt_raises_error(kwargs, expected_error):
             ),
             id='constant_position_single_fixation',
         ),
+        pytest.param(
+            {
+                'positions': pm.synthetic.step_function(length=100, steps=[0], values=[(0, 0)]),
+                'dispersion_threshold': 1,
+                'minimum_duration': 2,
+                'name': 'custom_fixation',
+            },
+            pm.events.EventDataFrame(
+                name='custom_fixation',
+                onsets=[0],
+                offsets=[99],
+            ),
+            id='constant_position_single_fixation_custom_name',
+        ),
         pytest.param(
             {
                 'positions': pm.synthetic.step_function(
diff --git a/tests/events/detection/ivt_test.py b/tests/events/detection/ivt_test.py
index 4fef433..f7f4bbe 100644
--- a/tests/events/detection/ivt_test.py
+++ b/tests/events/detection/ivt_test.py
@@ -186,6 +186,20 @@ def test_ivt_raise_error(kwargs, expected_error):
             ),
             id='constant_position_single_fixation',
         ),
+        pytest.param(
+            {
+                'positions': step_function(length=100, steps=[0], values=[(0, 0)]),
+                'velocity_threshold': 1,
+                'minimum_duration': 1,
+                'name': 'custom_fixation',
+            },
+            EventDataFrame(
+                name='custom_fixation',
+                onsets=[0],
+                offsets=[99],
+            ),
+            id='constant_position_single_fixation_custom_name',
+        ),
         pytest.param(
             {
                 'positions': step_function(
diff --git a/tests/events/detection/microsaccades_test.py b/tests/events/detection/microsaccades_test.py
index a2d7003..f5a10f7 100644
--- a/tests/events/detection/microsaccades_test.py
+++ b/tests/events/detection/microsaccades_test.py
@@ -100,6 +100,25 @@ def test_microsaccades_raises_error(kwargs, expected):
             ),
             id='two_steps_one_saccade',
         ),
+        pytest.param(
+            {
+                'positions': step_function(length=100, steps=[0], values=[(0, 0)]),
+                'velocities': step_function(
+                    length=100,
+                    steps=[40, 50],
+                    values=[(9, 9), (0, 0)],
+                    start_value=(0, 0),
+                ),
+                'threshold': 1e-5,
+                'name': 'custom_saccade',
+            },
+            EventDataFrame(
+                name='custom_saccade',
+                onsets=[40],
+                offsets=[49],
+            ),
+            id='two_steps_one_saccade_custom_name',
+        ),
         pytest.param(
             {
                 'positions': step_function(length=100, steps=[0], values=[(0, 0)]),