Repository: aeye-lab/pymovements
Pull request #63 (2022-11-03 11:18:16)

Add synthetic.step_function
## Description

Solves  # #61 

## Implemented changes
- [x] add step function with parameters for `length`, `steps`, `values` and `start_value`
- [x] add 6 test cases
- [x] add synthetic module to doc pages

## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## How Has This Been Tested?

- [x] Tested for zero length
- [x] Tested for a step at the beginning
- [x] Tested for step in the middle with different `start_value`
- [x] Tested for a sequence of 3 steps
- [x] Tested for ValueErrors (non-matching list lengths and unsorted steps)

## 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] I have added at least one docstring example for demonstration
- [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/reference/index.rst b/docs/source/reference/index.rst
index a4087aa..00485cb 100644
--- a/docs/source/reference/index.rst
+++ b/docs/source/reference/index.rst
@@ -13,4 +13,5 @@
     pymovements.events
     pymovements.paths
     pymovements.plot
-    pymovements.transforms
+    pymovements.synthetic
+    pymovements.transforms
\ No newline at end of file
diff --git a/src/pymovements/synthetic/__init__.py b/src/pymovements/synthetic/__init__.py
new file mode 100644
index 0000000..3ac39cc
--- /dev/null
+++ b/src/pymovements/synthetic/__init__.py
@@ -0,0 +1,6 @@
+"""
+This module holds functionality for generating synthetic eye gaze data.
+"""
+
+
+from pymovements.synthetic.step_function import step_function  # noqa: F401
diff --git a/src/pymovements/synthetic/step_function.py b/src/pymovements/synthetic/step_function.py
new file mode 100644
index 0000000..3ad5aae
--- /dev/null
+++ b/src/pymovements/synthetic/step_function.py
@@ -0,0 +1,68 @@
+"""
+This module holds the synthetic eye gaze step function.
+"""
+
+
+from __future__ import annotations
+
+import numpy as np
+
+
+def step_function(
+    length: int,
+    steps: list[int],
+    values: list[float],
+    start_value: float = 0,
+) -> np.ndarray:
+    """
+    Create a synthetic eye gaze by using a simple step function.
+
+    Parameters
+    ----------
+    length : int
+        Length of the output sequence.
+    steps : list[int]
+        Indices for each step to happen.
+    values : list[float]
+        Array values to set at each step.
+    start_value: int
+        Array value to start with.
+
+    Returns
+    -------
+    arr : np.ndarray
+        Output signal
+
+    Raises
+    ------
+    ValueError
+        If steps not sorted in ascending order or length of steps not equal to length of values.
+
+
+    Examples
+    --------
+        >>> step_function(
+        ...     length=10,
+        ...     steps=[2, 5, 9],
+        ...     values=[1, 2, 3],
+        ...     start_value=0,
+        ... )
+        array([0., 0., 1., 1., 1., 2., 2., 2., 2., 3.])
+    """
+    if len(steps) != len(values):
+        raise ValueError('length of steps not equal to length of values'
+                         f' ({len(steps)} != {len(values)})')
+
+    if sorted(steps) != steps:
+        raise ValueError('steps need to be sorted in ascending order.')
+
+    arr = np.ones(length) * start_value
+
+    # Iterate through all steps except the last.
+    for begin, end, value in zip(steps[:-1], steps[1:], values[:-1]):
+        arr[begin:end] = value
+
+    # Set value for each step until the end.
+    arr[steps[-1]:] = values[-1]
+
+    return arr


Test patch:
diff --git a/tests/synthetic/test_step_function.py b/tests/synthetic/test_step_function.py
new file mode 100644
index 0000000..5be234d
--- /dev/null
+++ b/tests/synthetic/test_step_function.py
@@ -0,0 +1,56 @@
+"""
+This module tests functionality of the synthetic eye gaze step function.
+"""
+
+
+import numpy as np
+import pytest
+
+from pymovements.synthetic import step_function
+
+
+@pytest.mark.parametrize(
+    'params, expected',
+    [
+        pytest.param(
+            {'length': 0, 'steps': [0], 'values': [0]},
+            {'value': np.array([])},
+            id='length_0_returns_empty_array',
+        ),
+        pytest.param(
+            {'length': 10, 'steps': [0], 'values': [1], 'start_value': 0},
+            {'value': np.ones(10)},
+            id='length_10_with_step_at_start',
+        ),
+        pytest.param(
+            {'length': 10, 'steps': [5], 'values': [0], 'start_value': 1},
+            {'value': np.concatenate([np.ones(5), np.zeros(5)])},
+            id='length_10_start_value_1_step_5_to_0',
+        ),
+        pytest.param(
+            {'length': 100, 'steps': [10, 50, 90], 'values': [1, 0, 20], 'start_value': 0},
+            {'value': np.concatenate([np.zeros(10), np.ones(40), np.zeros(40), np.ones(10) * 20])},
+            id='length_100_3_steps',
+        ),
+        pytest.param(
+            {'length': 100, 'steps': [10, 50, 90], 'values': [1, 0], 'start_value': 0},
+            {'exception': ValueError},
+            id='steps_values_unequal_length_raises_value_error',
+        ),
+        pytest.param(
+            {'length': 100, 'steps': [10, 90, 50], 'values': [1, 0, 1], 'start_value': 0},
+            {'exception': ValueError},
+            id='steps_not_sorted_raises_value_error',
+        ),
+    ]
+)
+def test_step_function(params, expected):
+    """Test step function."""
+
+    if 'exception' in expected:
+        with pytest.raises(expected['exception']):
+            step_function(**params)
+        return
+
+    arr = step_function(**params)
+    assert np.array_equal(arr, expected['value']), f'arr = {arr}, expected = {expected["value"]}'