Repository: aeye-lab/pymovements
Pull request #107 (2023-02-03 20:00:43)

Transforms: refactor center_origin to origin in pix2deg()
This PR changes some functionality from `pix2deg()`.

Before, `center_origin` was used to center pixel coordinates from the lower left to the center.
Now we have an `origin` argument to specify the location of the origin.
The meaning of the argument is much more understandable this way.
This way we will also be more flexible in the future.

Currently we still only support two cases:

- `center` which is the same as `center_origin=False` 
- `lower left` which is the same as `center_origin=True`


Patch:
diff --git a/README.md b/README.md
index 78b4f26..609bc4c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-<p align="center"><picture>
+<picture>
 <img width="110%" height="110%" src="./docs/source/_static/logo.svg" alt="pymovements">
-</picture></p>
+</picture>
 
 ---
 
@@ -67,4 +67,4 @@ following BibTex entry:
   publisher = {GitHub},
   howpublished = {\url{https://github.com/aeye-lab/pymovements}},
 }
-```
\ No newline at end of file
+```
diff --git a/pylintrc b/pylintrc
index 59e6a65..ac77eee 100644
--- a/pylintrc
+++ b/pylintrc
@@ -150,7 +150,7 @@ notes=FIXME,XXX,TODO
 [SIMILARITIES]
 
 # Minimum lines number of a similarity.
-min-similarity-lines=6
+min-similarity-lines=8
 
 # Ignore comments when computing similarities.
 ignore-comments=yes
diff --git a/src/pymovements/base.py b/src/pymovements/base.py
index 5f2ec27..5b9b4c1 100644
--- a/src/pymovements/base.py
+++ b/src/pymovements/base.py
@@ -6,6 +6,7 @@ from __future__ import annotations
 import numpy as np
 
 from pymovements.transforms import pix2deg
+from pymovements.transforms import pos2vel
 from pymovements.utils import checks
 from pymovements.utils.decorators import auto_str
 
@@ -18,7 +19,6 @@ class Screen:
 
     Attributes
     ----------
-
     width_px : int
         Screen width in pixels
     height_px : int
@@ -29,6 +29,8 @@ class Screen:
         Screen height in centimeters
     distance_cm : float
         Eye-to-screen distance in centimeters
+    origin : str
+        Specifies the screen location of the origin of the pixel coordinate system.
     x_max_dva : float
         Maximum screen x-coordinate in degrees of visual angle
     y_max_dva : float
@@ -40,7 +42,13 @@ class Screen:
 
     """
     def __init__(
-        self, width_px: int, height_px: int, width_cm: float, height_cm: float, distance_cm: float,
+        self,
+        width_px: int,
+        height_px: int,
+        width_cm: float,
+        height_cm: float,
+        distance_cm: float,
+        origin: str,
     ):
         """
         Initializes Screen.
@@ -57,6 +65,8 @@ class Screen:
             Screen height in centimeters
         distance_cm : float
             Eye-to-screen distance in centimeters
+        origin : str
+            Specifies the screen location of the origin of the pixel coordinate system.
 
         Examples
         --------
@@ -66,10 +76,11 @@ class Screen:
         ...     width_cm=38,
         ...     height_cm=30,
         ...     distance_cm=68,
+        ...     origin='lower left',
         ... )
         >>> print(screen)  # doctest: +NORMALIZE_WHITESPACE
         Screen(width_px=1280, height_px=1024, width_cm=38, height_cm=30, distance_cm=68,
-         x_max_dva=15.60, y_max_dva=12.43, x_min_dva=-15.60, y_min_dva=-12.43)
+        origin=lower left, x_max_dva=15.60, y_max_dva=12.43, x_min_dva=-15.60, y_min_dva=-12.43)
 
         """
         checks.check_no_zeros(width_px, "width_px")
@@ -83,17 +94,17 @@ class Screen:
         self.width_cm = width_cm
         self.height_cm = height_cm
         self.distance_cm = distance_cm
+        self.origin = origin
 
         # calculate screen boundary coordinates in degrees of visual angle
-        self.x_max_dva = pix2deg(width_px-1, width_px, width_cm, distance_cm)
-        self.y_max_dva = pix2deg(height_px-1, height_px, height_cm, distance_cm)
-        self.x_min_dva = pix2deg(0, width_px, width_cm, distance_cm)
-        self.y_min_dva = pix2deg(0, height_px, height_cm, distance_cm)
+        self.x_max_dva = pix2deg(width_px-1, width_px, width_cm, distance_cm, origin=origin)
+        self.y_max_dva = pix2deg(height_px-1, height_px, height_cm, distance_cm, origin=origin)
+        self.x_min_dva = pix2deg(0, width_px, width_cm, distance_cm, origin=origin)
+        self.y_min_dva = pix2deg(0, height_px, height_cm, distance_cm, origin=origin)
 
     def pix2deg(
             self,
             arr: float | list[float] | list[list[float]] | np.ndarray,
-            center_origin: bool = True,
     ) -> np.ndarray:
         """
         Converts pixel screen coordinates to degrees of visual angle.
@@ -102,8 +113,6 @@ class Screen:
         ----------
         arr : float, array_like
             Pixel coordinates to transform into degrees of visual angle
-        center_origin: bool
-            Center origin to (0,0) if positions origin is in bottom left corner
 
         Returns
         -------
@@ -123,6 +132,7 @@ class Screen:
         ...     width_cm=38,
         ...     height_cm=30,
         ...     distance_cm=68,
+        ...     origin='lower left',
         ... )
         """
         return pix2deg(
@@ -130,7 +140,7 @@ class Screen:
             screen_px=(self.width_px, self.height_px),
             screen_cm=(self.width_cm, self.height_cm),
             distance_cm=self.distance_cm,
-            center_origin=center_origin,
+            origin=self.origin,
         )
 
 
@@ -150,7 +160,7 @@ class Experiment:
     def __init__(
         self, screen_width_px: int, screen_height_px: int,
         screen_width_cm: float, screen_height_cm: float,
-        distance_cm: float, sampling_rate: float,
+        distance_cm: float, origin: str, sampling_rate: float,
     ):
         """
         Initializes Experiment.
@@ -158,16 +168,18 @@ class Experiment:
         Parameters
         ----------
 
-        width_px : int
+        screen_width_px : int
             Screen width in pixels
-        height_px : int
+        screen_height_px : int
             Screen height in pixels
-        width_cm : float
+        screen_width_cm : float
             Screen width in centimeters
-        height_cm : float
+        screen_height_cm : float
             Screen height in centimeters
         distance_cm : float
             Eye-to-screen distance in centimeters
+        origin : str
+            Specifies the screen location of the origin of the pixel coordinate system.
         sampling_rate : float
             Sampling rate in Hz
 
@@ -178,5 +190,40 @@ class Experiment:
             width_cm=screen_width_cm,
             height_cm=screen_height_cm,
             distance_cm=distance_cm,
+            origin=origin,
         )
         self.sampling_rate = sampling_rate
+
+    def pos2vel(
+        self,
+        arr: list[float] | list[list[float]] | np.ndarray,
+        method: str = 'smooth',
+        **kwargs,
+    ) -> np.ndarray:
+        """Compute velocity time series from 2-dimensional position time series.
+
+        Methods 'smooth', 'neighbors' and 'preceding' are adapted from
+            Engbert et al.: Microsaccade Toolbox 0.9.
+
+        Parameters
+        ----------
+        arr : array_like
+            Continuous 2D position time series
+        method : str
+            Computation method. See :func:`~transforms.pos2vel` for details, default: smooth.
+        kwargs: dict
+            Additional keyword arguments used for savitzky golay method.
+
+        Returns
+        -------
+        velocities : array_like
+            Velocity time series in input_unit / sec
+
+        Raises
+        ------
+        ValueError
+            If selected method is invalid, input array is too short for the
+            selected method or the sampling rate is below zero
+
+        """
+        return pos2vel(arr=arr, sampling_rate=self.sampling_rate, method=method, **kwargs)
diff --git a/src/pymovements/transforms.py b/src/pymovements/transforms.py
index cf6e281..f6010ef 100644
--- a/src/pymovements/transforms.py
+++ b/src/pymovements/transforms.py
@@ -16,7 +16,7 @@ def pix2deg(
         screen_px: float | list[float] | tuple[float, float] | np.ndarray,
         screen_cm: float | list[float] | tuple[float, float] | np.ndarray,
         distance_cm: float,
-        center_origin: bool = True,
+        origin: str,
 ) -> np.ndarray:
     """Converts pixel screen coordinates to degrees of visual angle.
 
@@ -30,22 +30,29 @@ def pix2deg(
         Screen dimension in centimeters
     distance_cm : float
         Eye-to-screen distance in centimeters
-    center_origin: bool
-        Center origin to (0,0) if arr origin is in bottom left corner
+    origin : str
+        Specifies the screen location of the origin of the pixel coordinate system. Valid values
+        are: center, lower left.
 
     Returns
     -------
-    degrees_of_visual_angle : np.ndarray
+    np.ndarray
         Coordinates in degrees of visual angle
 
     Raises
     ------
+    TypeError
+        If arr is None.
     ValueError
         If dimension screen_px or screen_cm don't match dimension of arr.
         If screen_px or screen_cm or one of its elements is zero.
         If distance_cm is zero.
+        If origin value is not supported.
 
     """
+    if arr is None:
+        raise TypeError("arr must not be None")
+
     checks.check_no_zeros(screen_px, "screen_px")
     checks.check_no_zeros(screen_cm, "screen_px")
     checks.check_no_zeros(distance_cm, "distance_cm")
@@ -54,7 +61,7 @@ def pix2deg(
     screen_px = np.array(screen_px)
     screen_cm = np.array(screen_cm)
 
-    # check basic arr dimensions
+    # Check basic arr dimensions.
     if arr.ndim not in [0, 1, 2]:
         raise ValueError(
             'Number of dimensions of arr must be either 0, 1 or 2'
@@ -83,18 +90,20 @@ def pix2deg(
         if screen_cm.shape != (2,):
             raise ValueError('arr is 4-dimensional, but screen_cm is not 2-dimensional')
 
-        # we have binocular data. double tile screen parameters.
+        # We have binocular data. Double tile screen parameters.
         screen_px = np.tile(screen_px, 2)
         screen_cm = np.tile(screen_cm, 2)
 
-    # compute eye-to-screen-distance in pixels
+    # Compute eye-to-screen-distance in pixels.
     distance_px = distance_cm * (screen_px / screen_cm)
 
-    # center screen coordinates such that 0 is in the center of the screen
-    if center_origin:
+    # If pixel coordinate system is not centered, shift pixel coordinate to the center.
+    if origin == "lower left":
         arr = arr - (screen_px - 1) / 2
+    elif origin != "center":
+        raise ValueError(f"origin {origin} is not supported.")
 
-    # 180 / pi transforms arc measure to degrees
+    # 180 / pi transforms arc measure to degrees.
     return np.arctan2(arr, distance_px) * 180 / np.pi
 
 
@@ -128,7 +137,7 @@ def pos2vel(
 
     Returns
     -------
-    velocities : array_like
+    np.ndarray
         Velocity time series in input_unit / sec
 
     Raises
diff --git a/src/pymovements/utils/archives.py b/src/pymovements/utils/archives.py
index 58baadf..3f7fe68 100644
--- a/src/pymovements/utils/archives.py
+++ b/src/pymovements/utils/archives.py
@@ -12,10 +12,13 @@ from collections.abc import Callable
 from pathlib import Path
 from typing import IO
 
+from pymovements.utils.paths import get_filepaths
+
 
 def extract_archive(
     source_path: Path,
     destination_path: Path | None = None,
+    recursive: bool = True,
     remove_finished: bool = False,
 ) -> Path:
     """Extract an archive.
@@ -29,6 +32,8 @@ def extract_archive(
     destination_path : Path, optional
         Path to the directory the file will be extracted to. If omitted, the directory of the file
         is used.
+    recursive : bool
+        Recursively extract archives which are included in extracted archive.
     remove_finished : bool
         If ``True``, remove the file after the extraction.
 
@@ -57,6 +62,26 @@ def extract_archive(
     if remove_finished:
         source_path.unlink()
 
+    if recursive:
+        # Get filepaths of all archives in extracted directory.
+        archive_extensions = [
+            *_ARCHIVE_EXTRACTORS.keys(),
+            *_ARCHIVE_TYPE_ALIASES.keys(),
+            *_COMPRESSED_FILE_OPENERS.keys(),
+        ]
+        archive_filepaths = get_filepaths(path=destination_path, extension=archive_extensions)
+
+        # Extract all found archives.
+        for archive_filepath in archive_filepaths:
+            extract_destination = archive_filepath.parent / archive_filepath.stem
+
+            extract_archive(
+                source_path=archive_filepath,
+                destination_path=extract_destination,
+                recursive=recursive,
+                remove_finished=remove_finished,
+            )
+
     return destination_path
 
 
diff --git a/src/pymovements/utils/downloads.py b/src/pymovements/utils/downloads.py
index 91433b1..7fe9e0a 100644
--- a/src/pymovements/utils/downloads.py
+++ b/src/pymovements/utils/downloads.py
@@ -20,6 +20,7 @@ def download_and_extract_archive(
     download_filename: str,
     extract_dirpath: Path | None = None,
     md5: str | None = None,
+    recursive: bool = True,
     remove_finished: bool = False,
 ) -> None:
     """Download and extract archive file.
@@ -36,8 +37,10 @@ def download_and_extract_archive(
         Path to directory where archive files will be extracted to.
     md5 : str, optional
         MD5 checksum of downloaded file. If None, do not check.
+    recursive : bool
+        Recursively extract archives which are included in extracted archive.
     remove_finished : bool
-        Remove downloaded file after successful extraction or decompression.
+        Remove downloaded file after successful extraction or decompression, default: False.
 
     Raises
     ------
@@ -56,7 +59,12 @@ def download_and_extract_archive(
         extract_dirpath = download_dirpath
 
     print(f"Extracting {archive_path.name} to {extract_dirpath}")
-    extract_archive(archive_path, extract_dirpath, remove_finished)
+    extract_archive(
+        source_path=archive_path,
+        destination_path=extract_dirpath,
+        recursive=recursive,
+        remove_finished=remove_finished,
+    )
 
 
 def download_file(
diff --git a/src/pymovements/utils/paths.py b/src/pymovements/utils/paths.py
index d0e8eb9..f08ad6c 100644
--- a/src/pymovements/utils/paths.py
+++ b/src/pymovements/utils/paths.py
@@ -8,8 +8,8 @@ from pathlib import Path
 
 
 def get_filepaths(
-        rootpath: str | Path,
-        extension: str | None = None,
+        path: str | Path,
+        extension: str | list[str] | None = None,
         regex: re.Pattern | None = None,
 ) -> list[Path]:
     """
@@ -18,9 +18,9 @@ def get_filepaths(
 
     Parameters
     ----------
-    rootpath: str | Path
+    path: str | Path
         Root path to be traversed.
-    extension: str, optional
+    extension: str, list of str, optional
         File extension to be filtered for.
     regex: re.Pattern, optional
         Regular expression filenames will be filtered for.
@@ -38,17 +38,20 @@ def get_filepaths(
     if extension is not None and regex is not None:
         raise ValueError("extension and regex are mutually exclusive")
 
-    rootpath = Path(rootpath)
-    if not rootpath.is_dir():
+    if extension is not None and isinstance(extension, str):
+        extension = [extension]
+
+    path = Path(path)
+    if not path.is_dir():
         return []
 
     filepaths = []
-    for childpath in rootpath.iterdir():
+    for childpath in path.iterdir():
         if childpath.is_dir():
-            filepaths.extend(get_filepaths(rootpath=childpath, extension=extension, regex=regex))
+            filepaths.extend(get_filepaths(path=childpath, extension=extension, regex=regex))
         else:
             # if extension specified and not matching, continue to next
-            if extension and childpath.suffix != extension:
+            if extension and childpath.suffix not in extension:
                 continue
             # if regex specified and not matching, continue to next
             if regex and not regex.match(childpath.name):


Test patch:
diff --git a/tests/test_transformations.py b/tests/test_transformations.py
index 5ab94ed..aa5fe43 100644
--- a/tests/test_transformations.py
+++ b/tests/test_transformations.py
@@ -23,7 +23,8 @@ screen_cm_2d = [100, 100]
                 'arr': None,
                 'screen_px': 1,
                 'screen_cm': 1,
-                'distance_cm': 1
+                'distance_cm': 1,
+                'origin': 'center',
             },
             TypeError,
             id='none_coords_raises_type_error'
@@ -33,7 +34,8 @@ screen_cm_2d = [100, 100]
                 'arr': 0,
                 'screen_px': None,
                 'screen_cm': 1,
-                'distance_cm': 1
+                'distance_cm': 1,
+                'origin': 'center',
             },
             TypeError,
             id='none_screen_px_raises_type_error'
@@ -43,7 +45,8 @@ screen_cm_2d = [100, 100]
                 'arr': 0,
                 'screen_px': 1,
                 'screen_cm': None,
-                'distance_cm': 1
+                'distance_cm': 1,
+                'origin': 'center',
             },
             TypeError,
             id='none_screen_cm_raises_type_error'
@@ -53,7 +56,8 @@ screen_cm_2d = [100, 100]
                 'arr': 0,
                 'screen_px': 1,
                 'screen_cm': 1,
-                'distance_cm': None
+                'distance_cm': None,
+                'origin': 'center',
             },
             TypeError,
             id='none_distance_cm_raises_type_error'
@@ -63,7 +67,8 @@ screen_cm_2d = [100, 100]
                 'arr': 0,
                 'screen_px': 0,
                 'screen_cm': 1,
-                'distance_cm': 1
+                'distance_cm': 1,
+                'origin': 'center',
             },
             ValueError,
             id='zero_screen_px_raises_value_error'
@@ -73,7 +78,8 @@ screen_cm_2d = [100, 100]
                 'arr': 0,
                 'screen_px': 1,
                 'screen_cm': 0,
-                'distance_cm': 1
+                'distance_cm': 1,
+                'origin': 'center',
             },
             ValueError,
             id='zero_screen_cm_raises_value_error'
@@ -83,7 +89,8 @@ screen_cm_2d = [100, 100]
                 'arr': 0,
                 'screen_px': 1,
                 'screen_cm': 1,
-                'distance_cm': 0
+                'distance_cm': 0,
+                'origin': 'center',
             },
             ValueError,
             id='zero_distance_cm_raises_value_error'
@@ -94,7 +101,7 @@ screen_cm_2d = [100, 100]
                 'screen_px': screen_px_2d,
                 'screen_cm': screen_cm_2d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             ValueError,
             id='rank_3_tensor_raises_value_error'
@@ -105,7 +112,7 @@ screen_cm_2d = [100, 100]
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_px_2d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             ValueError,
             id='list_coords_2d_screen_px_1d_raises_value_error'
@@ -116,7 +123,7 @@ screen_cm_2d = [100, 100]
                 'screen_px': screen_px_2d,
                 'screen_cm': screen_px_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             ValueError,
             id='list_coords_2d_screen_cm_1d_raises_value_error'
@@ -127,7 +134,7 @@ screen_cm_2d = [100, 100]
                 'screen_px': screen_px_2d,
                 'screen_cm': screen_px_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             ValueError,
             id='list_coords_1d_screen_px_2d_raises_value_error'
@@ -138,7 +145,7 @@ screen_cm_2d = [100, 100]
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_px_2d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             ValueError,
             id='list_coords_1d_screen_cm_2d_raises_value_error'
@@ -149,7 +156,7 @@ screen_cm_2d = [100, 100]
                 'screen_px': [1, 1, 1],
                 'screen_cm': [1, 1, 1],
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             ValueError,
             id='list_coords_3d_raises_value_error'
@@ -170,7 +177,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             0,
             id='zero_coord_without_center_origin_returns_zero'
@@ -181,7 +188,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': True
+                'origin': 'lower left',
             },
             0,
             id='center_coord_with_center_origin_returns_zero'
@@ -192,7 +199,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2,
-                'center_origin': False
+                'origin': 'center',
             },
             45,
             id='isosceles_triangle_without_center_origin_returns_45'
@@ -203,7 +210,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2,
-                'center_origin': True
+                'origin': 'lower left',
             },
             45,
             id='isosceles_triangle_with_center_origin_returns_45'
@@ -214,7 +221,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2,
-                'center_origin': True
+                'origin': 'lower left',
             },
             -45,
             id='isosceles_triangle_left_with_center_origin_returns_minus45'
@@ -225,7 +232,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_2d,
                 'screen_cm': screen_cm_2d,
                 'distance_cm': screen_px_2d[0] / 2,
-                'center_origin': False
+                'origin': 'center',
             },
             45,
             id='nparray_of_isosceles_triangle_without_center_origin_returns_45'
@@ -236,7 +243,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             pytest.approx(26.565, abs=1e-4),
             id='ankathet_half_without_center_origin_returns_26565'
@@ -247,7 +254,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': True
+                'origin': 'lower left',
             },
             pytest.approx(26.565, abs=1e-4),
             id='ankathet_half_with_center_origin_returns_26565'
@@ -258,7 +265,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2 / np.sqrt(3),
-                'center_origin': False
+                'origin': 'center',
             },
             pytest.approx(60),
             id='ankathet_sqrt_3_without_center_origin_returns_60'
@@ -269,7 +276,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2 / np.sqrt(3),
-                'center_origin': True
+                'origin': 'lower left',
             },
             pytest.approx(60),
             id='ankathet_sqrt_3_with_center_origin_returns_60'
@@ -280,7 +287,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2 * np.sqrt(3),
-                'center_origin': False
+                'origin': 'center',
             },
             pytest.approx(30),
             id='opposite_sqrt_3_without_center_origin_returns_30'
@@ -291,7 +298,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d / 2 * np.sqrt(3),
-                'center_origin': True
+                'origin': 'lower left',
             },
             pytest.approx(30),
             id='opposite_sqrt_3_with_center_origin_returns_30'
@@ -302,7 +309,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             np.array([0.0] * n_coords),
             id='list_of_zero_coords_1d'
@@ -313,7 +320,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_1d,
                 'screen_cm': screen_cm_1d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             np.array([0.0] * n_coords),
             id='nparray_of_zero_coords_1d'
@@ -324,7 +331,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_2d,
                 'screen_cm': screen_cm_2d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             np.array([[0.0, 0.0]] * n_coords),
             id='list_of_zero_coords_2d'
@@ -335,7 +342,7 @@ def test_pix2deg_raises_error(kwargs, expected_error):
                 'screen_px': screen_px_2d,
                 'screen_cm': screen_cm_2d,
                 'distance_cm': screen_cm_1d,
-                'center_origin': False
+                'origin': 'center',
             },
             np.array([[0.0, 0.0]] * n_coords),
             id='nparray_of_zero_coords_2d'