{ "cells": [ { "cell_type": "markdown", "id": "5ad52697", "metadata": {}, "source": [ "# Processing monocular and binocular data" ] }, { "cell_type": "code", "execution_count": 1, "id": "c7655f3e", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:53.191056Z", "iopub.status.busy": "2026-11-09T18:03:53.190627Z", "iopub.status.idle": "2026-11-09T18:03:53.461734Z", "shell.execute_reply": "2026-11-09T18:03:53.460165Z" } }, "outputs": [], "source": [ "import pypillometry as pp\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "fe099bbc", "metadata": {}, "source": [ "Data in `pypillometry` can contain different *variables* from different *eyes*. The variables and eyes supported when importing raw data are\n", "\n", "- `left_x`, `right_x` (x-coordinate in screen coordinates from the eyetracker)\n", "- `left_y`, `right_y` (y-coordinate in screen coordinates from the eyetracker)\n", "- `left_pupil`, `right_pupil` (pupil size from left and right eye)\n", "\n", "All of them are held in an `EyeData` object. Any of these variables can be left out, but at least one must be given, and the `x` and `y` of an eye go together:\n", "\n", "- pupil data only: at least one of `left_pupil`, `right_pupil` (or both)\n", "- gaze data only: at least one of `(left_x, left_y)` and/or `(right_x, right_y)`\n", "- both: `x`, `y` and `pupil` from at least one eye\n", "\n", "For example, let's simulate some basic data:" ] }, { "cell_type": "code", "execution_count": 2, "id": "b2605bfb", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:53.465343Z", "iopub.status.busy": "2026-11-09T18:03:53.464944Z", "iopub.status.idle": "2026-11-09T18:03:53.480540Z", "shell.execute_reply": "2026-11-09T18:03:53.479442Z" } }, "outputs": [], "source": [ "rng = np.random.default_rng(42)\n", "n = 1000\n", "left_x, right_x = rng.normal(size=n), rng.normal(size=n)\n", "left_y, right_y = rng.normal(size=n), rng.normal(size=n)\n", "left_pupil, right_pupil = rng.normal(size=n), rng.normal(size=n)\n", "time = np.arange(n) # time in ms\n", "\n", "# these are all ok\n", "dpupil = pp.EyeData(left_pupil=left_pupil, right_pupil=right_pupil, time=time)\n", "dgaze = pp.EyeData(left_x=left_x, left_y=left_y, right_x=right_x, right_y=right_y, time=time)\n", "deye = pp.EyeData(left_x=left_x, left_y=left_y, left_pupil=left_pupil, time=time)" ] }, { "cell_type": "markdown", "id": "1acb48b1", "metadata": {}, "source": [ "The `x` and `y` of an eye go together, so giving only one of them (here, `left_x` without `left_y`) fails:" ] }, { "cell_type": "code", "execution_count": 3, "id": "ebd2d201", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:53.484058Z", "iopub.status.busy": "2026-11-09T18:03:53.483682Z", "iopub.status.idle": "2026-11-09T18:03:53.773780Z", "shell.execute_reply": "2026-11-09T18:03:53.772489Z" }, "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "ValueError", "evalue": "left_x and left_y must be provided together", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m pp.EyeData(left_x=left_x, time=time)\n", "\u001b[36mFile \u001b[39m\u001b[32m~/Nextcloud/work/projects/pupil/pypillometry/pypillometry/eyedata/eyedata.py:245\u001b[39m, in \u001b[36mEyeData.__init__\u001b[39m\u001b[34m(self, time, left_x, left_y, left_pupil, right_x, right_y, right_pupil, event_onsets, event_labels, events, sampling_rate, experimental_setup, name, calibration, fill_time_discontinuities, keep_orig, info, inplace)\u001b[39m\n\u001b[32m 243\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m eye, x, y \u001b[38;5;129;01min\u001b[39;00m ((\u001b[33m\"\u001b[39m\u001b[33mleft\u001b[39m\u001b[33m\"\u001b[39m, left_x, left_y), (\u001b[33m\"\u001b[39m\u001b[33mright\u001b[39m\u001b[33m\"\u001b[39m, right_x, right_y)):\n\u001b[32m 244\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m (x \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m) != (y \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[32m--> \u001b[39m\u001b[32m245\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00meye\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m_x and \u001b[39m\u001b[38;5;132;01m{\u001b[39;00meye\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m_y must be provided together\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 246\u001b[39m \u001b[38;5;28mself\u001b[39m.data = EyeDataDict(left_x=left_x, left_y=left_y, left_pupil=left_pupil,\n\u001b[32m 247\u001b[39m right_x=right_x, right_y=right_y, right_pupil=right_pupil)\n\u001b[32m 249\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m events \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m (event_onsets \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mor\u001b[39;00m event_labels \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m):\n", "\u001b[31mValueError\u001b[39m: left_x and left_y must be provided together" ] } ], "source": [ "pp.EyeData(left_x=left_x, time=time)" ] }, { "cell_type": "markdown", "id": "e84c32e3", "metadata": {}, "source": [ "Once the data is loaded, we can check which variables and eyes are available using the `.eyes` and `.variables` attributes:" ] }, { "cell_type": "code", "execution_count": 4, "id": "53ff9ec0", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:53.777847Z", "iopub.status.busy": "2026-11-09T18:03:53.777530Z", "iopub.status.idle": "2026-11-09T18:03:53.786716Z", "shell.execute_reply": "2026-11-09T18:03:53.785248Z" } }, "outputs": [ { "data": { "text/plain": [ "(['left'], ['x', 'y', 'pupil'])" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "deye.eyes, deye.variables" ] }, { "cell_type": "markdown", "id": "5a0a271e", "metadata": {}, "source": [ "Simply printing an object also shows which data are available and gives a glimpse into the data:" ] }, { "cell_type": "code", "execution_count": 5, "id": "264beb66", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:53.790081Z", "iopub.status.busy": "2026-11-09T18:03:53.789562Z", "iopub.status.idle": "2026-11-09T18:03:54.396032Z", "shell.execute_reply": "2026-11-09T18:03:54.394528Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "

EyeData — dobogaru

\n", "\n", "
\n", "
\n", "
Samples
\n", "
1,000
\n", "
\n", "
\n", "
Sampling rate
\n", "
1000.0 Hz
\n", "
\n", "
\n", "
Duration
\n", "
0.02 min (start 0.00, end 0.02)
\n", "
\n", "
\n", "
Eyes
\n", "
left
\n", "
\n", "
\n", "
Variables
\n", "
left_x, left_y, left_pupil
\n", "
\n", "
\n", "
Events
\n", "
0
\n", "
\n", "
\n", "
Size
\n", "
39.3KiB
\n", "
\n", "
\n", "\n", "\n", "
\n", " Signal overview\n", "
\n", "
\n", "
left_pupil
\n", " \"left_pupil\n", "
\n", "
\n", "
left_x
\n", " \"left_x\n", "
\n", "
\n", "
left_y
\n", " \"left_y\n", "
\n", "
\n", "
\n", "\n", "\n", "\n", "
\n", " Data glimpse\n", "
EyeDataDict(vars=3,n=1000,shape=(1000,)): \n",
       "  left_x (float64): 0.3047, -1.04, 0.7505, 0.9406, -1.951...\n",
       "  left_y (float64): -0.452, -0.6659, 0.434, 0.2519, -1.4048...\n",
       "  left_pupil (float64): 0.2532, 0.8952, 0.2733, 2.2388, 1.4298...\n",
       "
\n", "
\n", "\n", "
\n" ], "text/plain": [ "EyeData(dobogaru, 39.3KiB):\n", " n : 1000\n", " sampling_rate : 1000.0\n", " eyes : ['left']\n", " data : ['left_x', 'left_y', 'left_pupil']\n", " nevents : 0\n", " experimental_setup: not set\n", " calibration : not available\n", " duration_minutes : 0.01667\n", " start_min : 0.0\n", " end_min : 0.01665\n", " params : {}\n", " nblinks : {}\n", " blinks : {}\n", " glimpse : EyeDataDict(vars=3,n=1000,shape=(1000,)): \n", " left_x (float64): 0.3047, -1.04, 0.7505, 0.9406, -1.951...\n", " left_y (float64): -0.452, -0.6659, 0.434, 0.2519, -1.4048...\n", " left_pupil (float64): 0.2532, 0.8952, 0.2733, 2.2388, 1.4298...\n", "\n", " History:\n", " *" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "deye" ] }, { "cell_type": "code", "execution_count": 6, "id": "6f1eb229", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:54.398727Z", "iopub.status.busy": "2026-11-09T18:03:54.398271Z", "iopub.status.idle": "2026-11-09T18:03:54.790089Z", "shell.execute_reply": "2026-11-09T18:03:54.789204Z" } }, "outputs": [ { "data": { "text/plain": [ "(['left', 'right'], ['x', 'y', 'pupil'])" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "d = pp.get_example_data(\"rlmw_002_short\")\n", "d.eyes, d.variables" ] }, { "cell_type": "markdown", "id": "719aae40", "metadata": {}, "source": [ "Almost all of `pypillometry`'s functions have keyword arguments `eyes=` and `variables=` that specify which eyes/variables to operate on. By default, all variables and eyes are processed.\n", "\n", "For example, we can run the `scale()` function that re-scales the data to have mean 0 and standard deviation 1.\n", "Here, we use the context manager `pp.loglevel(\"DEBUG\")` to see what `pypillometry` does internally:" ] }, { "cell_type": "code", "execution_count": 7, "id": "9ee784a6", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:54.793091Z", "iopub.status.busy": "2026-11-09T18:03:54.792790Z", "iopub.status.idle": "2026-11-09T18:03:54.816165Z", "shell.execute_reply": "2026-11-09T18:03:54.815078Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:54\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mscale\u001b[0m:\u001b[36m2513\u001b[0m | \u001b[34m\u001b[1mchannels: left_x, left_y, left_pupil\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:54\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mscale\u001b[0m:\u001b[36m2529\u001b[0m | \u001b[34m\u001b[1mcenter: {'left_x': -0.028891550995946837, 'left_y': 0.03370120480230512, 'left_pupil': -0.02135748147595793}\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:54\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mscale\u001b[0m:\u001b[36m2530\u001b[0m | \u001b[34m\u001b[1mscale: {'left_x': 0.988722355396508, 'left_y': 1.0161916099991923, 'left_pupil': 0.9978409919199771}\u001b[0m\n" ] } ], "source": [ "with pp.loglevel(\"DEBUG\"):\n", " deye.scale(eyes=\"left\")" ] }, { "cell_type": "markdown", "id": "9a7b8c12", "metadata": {}, "source": [ "The output shows that all variables of the left eye have been processed.\n", "\n", "## Which functions work on which data?\n", "\n", "Not all of `pypillometry`'s functions can be applied to all variables. The functions that are specific to pupil data are in the namespace `d.pupil` (e.g., `d.pupil.blinks_detect()`), those that only work on gaze (x/y) data in `d.gaze` (e.g., `d.gaze.mask_offscreen()`). The methods of `EyeData` itself operate on all variables (which may or may not make sense, it is up to you to check!).\n", "\n", "Functions that work only on pupillometric data raise an error for data without pupil size:" ] }, { "cell_type": "code", "execution_count": 8, "id": "6c93e926", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:54.819622Z", "iopub.status.busy": "2026-11-09T18:03:54.819197Z", "iopub.status.idle": "2026-11-09T18:03:55.442637Z", "shell.execute_reply": "2026-11-09T18:03:55.441892Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "

EyeData — tumedihe

\n", "\n", "
\n", "
\n", "
Samples
\n", "
1,000
\n", "
\n", "
\n", "
Sampling rate
\n", "
1000.0 Hz
\n", "
\n", "
\n", "
Duration
\n", "
0.02 min (start 0.00, end 0.02)
\n", "
\n", "
\n", "
Eyes
\n", "
left, right
\n", "
\n", "
\n", "
Variables
\n", "
left_pupil, right_pupil
\n", "
\n", "
\n", "
Events
\n", "
0
\n", "
\n", "
\n", "
Blinks
\n", "
left_pupil: 0, right_pupil: 0
\n", "
\n", "
\n", "
Size
\n", "
30.0KiB
\n", "
\n", "
\n", "\n", "\n", "
\n", " Signal overview\n", "
\n", "
\n", "
left_pupil
\n", " \"left_pupil\n", "
\n", "
\n", "
right_pupil
\n", " \"right_pupil\n", "
\n", "
\n", "
\n", "\n", "\n", "\n", "
\n", " Data glimpse\n", "
EyeDataDict(vars=2,n=1000,shape=(1000,)): \n",
       "  left_pupil (float64): 0.2532, 0.8952, 0.2733, 2.2388, 1.4298...\n",
       "  right_pupil (float64): -0.258, -0.2995, -0.6967, 0.4285, 0.2346...\n",
       "
\n", "
\n", "\n", "
\n", " Recent history\n", "
    \n", "
  1. pupil.blinks_detect()
  2. \n", "
\n", "
\n", "
\n" ], "text/plain": [ "EyeData(tumedihe, 30.0KiB):\n", " n : 1000\n", " sampling_rate : 1000.0\n", " eyes : ['left', 'right']\n", " data : ['left_pupil', 'right_pupil']\n", " nevents : 0\n", " experimental_setup: not set\n", " calibration : not available\n", " duration_minutes : 0.01667\n", " start_min : 0.0\n", " end_min : 0.01665\n", " params : {}\n", " nblinks : {'left_pupil': 0, 'right_pupil': 0}\n", " blinks : {'left_pupil': {'n': 0, 'mean': nan, 'sd': nan, 'min': nan, 'max': nan, 'total_duration': 0.0, 'units': 'ms'}, 'right_pupil': {'n': 0, 'mean': nan, 'sd': nan, 'min': nan, 'max': nan, 'total_duration': 0.0, 'units': 'ms'}}\n", " glimpse : EyeDataDict(vars=2,n=1000,shape=(1000,)): \n", " left_pupil (float64): 0.2532, 0.8952, 0.2733, 2.2388, 1.4298...\n", " right_pupil (float64): -0.258, -0.2995, -0.6967, 0.4285, 0.2346...\n", "\n", " History:\n", " *\n", " └ pupil.blinks_detect()" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dpupil.pupil.blinks_detect() # works fine" ] }, { "cell_type": "code", "execution_count": 9, "id": "9ddf7b72", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:55.445294Z", "iopub.status.busy": "2026-11-09T18:03:55.445058Z", "iopub.status.idle": "2026-11-09T18:03:55.550370Z", "shell.execute_reply": "2026-11-09T18:03:55.549256Z" }, "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "ValueError", "evalue": "no pupil data; available channels: left_x, left_y, right_x, right_y", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m dgaze.pupil.blinks_detect() \u001b[38;5;66;03m# fails\u001b[39;00m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/Nextcloud/work/projects/pupil/pypillometry/pypillometry/eyedata/pipes.py:50\u001b[39m, in \u001b[36mstep..wrapper\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 48\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m, Namespace) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m._data \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 49\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m Step(wrapper, args, kwargs, namespace=\u001b[38;5;28mself\u001b[39m._name)\n\u001b[32m---> \u001b[39m\u001b[32m50\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mfunc\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43margs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/Nextcloud/work/projects/pupil/pypillometry/pypillometry/eyedata/history.py:70\u001b[39m, in \u001b[36mkeephistory..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 68\u001b[39m _history_depth += \u001b[32m1\u001b[39m\n\u001b[32m 69\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m70\u001b[39m obj=\u001b[30;43mfunc\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43margs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 71\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 72\u001b[39m _history_depth -= \u001b[32m1\u001b[39m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/Nextcloud/work/projects/pupil/pypillometry/pypillometry/eyedata/pupil.py:557\u001b[39m, in \u001b[36mPupilNamespace.blinks_detect\u001b[39m\u001b[34m(self, eyes, min_duration, blink_val, winsize, vel_onset, vel_offset, vel_onset_min_duration, vel_offset_min_duration, max_duration, strategies, ignore_existing_mask, inplace)\u001b[39m\n\u001b[32m 467\u001b[39m \u001b[38;5;129m@step\u001b[39m\n\u001b[32m 468\u001b[39m \u001b[38;5;129m@keephistory\u001b[39m\n\u001b[32m 469\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mblinks_detect\u001b[39m(\u001b[38;5;28mself\u001b[39m, eyes=[], min_duration:\u001b[38;5;28mfloat\u001b[39m=\u001b[32m20\u001b[39m, blink_val:\u001b[38;5;28mfloat\u001b[39m=\u001b[32m0\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 473\u001b[39m strategies: \u001b[38;5;28mlist\u001b[39m=[\u001b[33m\"\u001b[39m\u001b[33mzero\u001b[39m\u001b[33m\"\u001b[39m,\u001b[33m\"\u001b[39m\u001b[33mvelocity\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 474\u001b[39m ignore_existing_mask=\u001b[38;5;28;01mFalse\u001b[39;00m, inplace=\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[32m 475\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 476\u001b[39m \u001b[33;03m Detect blinks in the pupillary signal using several strategies.\u001b[39;00m\n\u001b[32m 477\u001b[39m \u001b[33;03m First, blinks are detected as consecutive sequence of `blink_val` \u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 555\u001b[39m \u001b[33;03m >>> d.get_blinks(variables=\"pupil\", units=\"ms\") # doctest: +SKIP\u001b[39;00m\n\u001b[32m 556\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m557\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_d\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_require\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43mpupil\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 558\u001b[39m obj = \u001b[38;5;28mself\u001b[39m._d._get_inplace(inplace)\n\u001b[32m 559\u001b[39m eyes=\u001b[38;5;28mself\u001b[39m._d._eyes_with(eyes, \u001b[33m\"\u001b[39m\u001b[33mpupil\u001b[39m\u001b[33m\"\u001b[39m)\n", "\u001b[36mFile \u001b[39m\u001b[32m~/Nextcloud/work/projects/pupil/pypillometry/pypillometry/eyedata/eyedata.py:562\u001b[39m, in \u001b[36mEyeData._require\u001b[39m\u001b[34m(self, *variables)\u001b[39m\n\u001b[32m 560\u001b[39m missing = [var \u001b[38;5;28;01mfor\u001b[39;00m var \u001b[38;5;129;01min\u001b[39;00m variables \u001b[38;5;28;01mif\u001b[39;00m var \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m available]\n\u001b[32m 561\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m missing:\n\u001b[32m--> \u001b[39m\u001b[32m562\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mno \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m data; available channels: \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 563\u001b[39m % (\u001b[33m\"\u001b[39m\u001b[33m, \u001b[39m\u001b[33m\"\u001b[39m.join(missing), \u001b[33m\"\u001b[39m\u001b[33m, \u001b[39m\u001b[33m\"\u001b[39m.join(\u001b[38;5;28mself\u001b[39m.data.keys()) \u001b[38;5;129;01mor\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mnone\u001b[39m\u001b[33m\"\u001b[39m))\n", "\u001b[31mValueError\u001b[39m: no pupil data; available channels: left_x, left_y, right_x, right_y" ] } ], "source": [ "dgaze.pupil.blinks_detect() # fails" ] }, { "cell_type": "markdown", "id": "e2423269", "metadata": {}, "source": [ "Functions that are methods of `EyeData` itself (not of `d.pupil` or `d.gaze`) work for all data:" ] }, { "cell_type": "code", "execution_count": 10, "id": "de030ea2", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:55.554432Z", "iopub.status.busy": "2026-11-09T18:03:55.553989Z", "iopub.status.idle": "2026-11-09T18:03:55.624502Z", "shell.execute_reply": "2026-11-09T18:03:55.623368Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "

EyeData — vosawomi

\n", "\n", "
\n", "
\n", "
Samples
\n", "
1,000
\n", "
\n", "
\n", "
Sampling rate
\n", "
1000.0 Hz
\n", "
\n", "
\n", "
Duration
\n", "
0.02 min (start 0.00, end 0.02)
\n", "
\n", "
\n", "
Eyes
\n", "
left, right
\n", "
\n", "
\n", "
Variables
\n", "
left_x, left_y, right_x, right_y
\n", "
\n", "
\n", "
Events
\n", "
0
\n", "
\n", "
\n", "
Size
\n", "
49.8KiB
\n", "
\n", "
\n", "\n", "\n", "
\n", " Signal overview\n", "
\n", "
\n", "
left_x
\n", " \"left_x\n", "
\n", "
\n", "
left_y
\n", " \"left_y\n", "
\n", "
\n", "
right_x
\n", " \"right_x\n", "
\n", "
\n", "
right_y
\n", " \"right_y\n", "
\n", "
\n", "
\n", "\n", "\n", "\n", "
\n", " Data glimpse\n", "
EyeDataDict(vars=4,n=1000,shape=(1000,)): \n",
       "  left_x (float64): 0.3374, -1.0226, 0.7882, 0.9805, -1.9441...\n",
       "  left_y (float64): -0.4779, -0.6884, 0.3939, 0.2147, -1.4156...\n",
       "  right_x (float64): 0.02179, -0.6389, -0.3285, 0.7054, 0.0832...\n",
       "  right_y (float64): 1.2814, 0.7062, 2.0163, -1.5846, -0.2704...\n",
       "
\n", "
\n", "\n", "
\n", " Recent history\n", "
    \n", "
  1. scale()
  2. \n", "
\n", "
\n", "
\n" ], "text/plain": [ "EyeData(vosawomi, 49.8KiB):\n", " n : 1000\n", " sampling_rate : 1000.0\n", " eyes : ['left', 'right']\n", " data : ['left_x', 'left_y', 'right_x', 'right_y']\n", " nevents : 0\n", " experimental_setup: not set\n", " calibration : not available\n", " duration_minutes : 0.01667\n", " start_min : 0.0\n", " end_min : 0.01665\n", " params : {scale: {...}}\n", " nblinks : {}\n", " blinks : {}\n", " glimpse : EyeDataDict(vars=4,n=1000,shape=(1000,)): \n", " left_x (float64): 0.3374, -1.0226, 0.7882, 0.9805, -1.9441...\n", " left_y (float64): -0.4779, -0.6884, 0.3939, 0.2147, -1.4156...\n", " right_x (float64): 0.02179, -0.6389, -0.3285, 0.7054, 0.0832...\n", " right_y (float64): 1.2814, 0.7062, 2.0163, -1.5846, -0.2704...\n", "\n", " History:\n", " *\n", " └ scale()" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dpupil.scale() # works\n", "dgaze.scale() # works" ] }, { "cell_type": "markdown", "id": "271c1bf4", "metadata": {}, "source": [ "## Creating new variables or eyes\n", "\n", "In some cases, new variables or \"eyes\" can be created. For example, we might want to reduce a binocular dataset to one where we average the timeseries from the two eyes. In that case, we can use the function `merge_eyes()`:" ] }, { "cell_type": "code", "execution_count": 11, "id": "0a0c9c07", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:55.627113Z", "iopub.status.busy": "2026-11-09T18:03:55.626721Z", "iopub.status.idle": "2026-11-09T18:03:55.671018Z", "shell.execute_reply": "2026-11-09T18:03:55.669665Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", "

EyeData — tumedihe

\n", "\n", "
\n", "
\n", "
Samples
\n", "
1,000
\n", "
\n", "
\n", "
Sampling rate
\n", "
1000.0 Hz
\n", "
\n", "
\n", "
Duration
\n", "
0.02 min (start 0.00, end 0.02)
\n", "
\n", "
\n", "
Eyes
\n", "
left, right, mean
\n", "
\n", "
\n", "
Variables
\n", "
left_pupil, right_pupil, mean_pupil
\n", "
\n", "
\n", "
Events
\n", "
0
\n", "
\n", "
\n", "
Blinks
\n", "
left_pupil: 0, right_pupil: 0, mean_pupil: 0
\n", "
\n", "
\n", "
Size
\n", "
40.9KiB
\n", "
\n", "
\n", "\n", "\n", "
\n", " Signal overview\n", "
\n", "
\n", "
left_pupil
\n", " \"left_pupil\n", "
\n", "
\n", "
mean_pupil
\n", " \"mean_pupil\n", "
\n", "
\n", "
right_pupil
\n", " \"right_pupil\n", "
\n", "
\n", "
\n", "\n", "\n", "\n", "
\n", " Data glimpse\n", "
EyeDataDict(vars=3,n=1000,shape=(1000,)): \n",
       "  left_pupil (float64): 0.2752, 0.9186, 0.2953, 2.2651, 1.4543...\n",
       "  right_pupil (float64): -0.3013, -0.342, -0.7317, 0.3722, 0.182...\n",
       "  mean_pupil (float64): -0.01307, 0.2883, -0.2182, 1.3186, 0.8181...\n",
       "
\n", "
\n", "\n", "
\n", " Recent history\n", "
    \n", "
  1. pupil.blinks_detect()
  2. \n", "
  3. scale()
  4. \n", "
  5. merge_eyes(eyes=['left', 'right'],variables=['pupil'],method='mean')
  6. \n", "
\n", "
\n", "
\n" ], "text/plain": [ "EyeData(tumedihe, 40.9KiB):\n", " n : 1000\n", " sampling_rate : 1000.0\n", " eyes : ['left', 'right', 'mean']\n", " data : ['left_pupil', 'right_pupil', 'mean_pupil']\n", " nevents : 0\n", " experimental_setup: not set\n", " calibration : not available\n", " duration_minutes : 0.01667\n", " start_min : 0.0\n", " end_min : 0.01665\n", " params : {scale: {...}}\n", " nblinks : {'left_pupil': 0, 'right_pupil': 0, 'mean_pupil': 0}\n", " blinks : {'left_pupil': {'n': 0, 'mean': nan, 'sd': nan, 'min': nan, 'max': nan, 'total_duration': 0.0, 'units': 'ms'}, 'right_pupil': {'n': 0, 'mean': nan, 'sd': nan, 'min': nan, 'max': nan, 'total_duration': 0.0, 'units': 'ms'}, 'mean_pupil': {'n': 0, 'mean': nan, 'sd': nan, 'min': nan, 'max': nan, 'total_duration': 0.0, 'units': 'ms'}}\n", " glimpse : EyeDataDict(vars=3,n=1000,shape=(1000,)): \n", " left_pupil (float64): 0.2752, 0.9186, 0.2953, 2.2651, 1.4543...\n", " right_pupil (float64): -0.3013, -0.342, -0.7317, 0.3722, 0.182...\n", " mean_pupil (float64): -0.01307, 0.2883, -0.2182, 1.3186, 0.8181...\n", "\n", " History:\n", " *\n", " └ pupil.blinks_detect()\n", " └ scale()\n", " └ merge_eyes(eyes=['left', 'right'],variables=['pupil'],method='mean')" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dpupil.merge_eyes(eyes=[\"left\", \"right\"], variables=[\"pupil\"], method=\"mean\")" ] }, { "cell_type": "markdown", "id": "65b613b1", "metadata": {}, "source": [ "We can see that a new \"eye\" called `mean` with variable `pupil` (i.e., `mean_pupil`) has been created. The original data `left_pupil` and `right_pupil` have been kept (this can be changed by using `keep_eyes=False`).\n", "\n", "In other cases, the package creates new variables. For example, the function `pupil.estimate_baseline()` estimates the tonic fluctuations in the pupil (see [Mittner & Groot, 2025](https://doi.org/10.3758/s13428-025-02755-7)) and creates a new variable `_baseline`. The baseline estimation does not work with missing data, so we first detect and interpolate blinks:" ] }, { "cell_type": "code", "execution_count": 12, "id": "794c407c", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:55.673425Z", "iopub.status.busy": "2026-11-09T18:03:55.673108Z", "iopub.status.idle": "2026-11-09T18:03:56.575373Z", "shell.execute_reply": "2026-11-09T18:03:56.574516Z" } }, "outputs": [ { "data": { "text/plain": [ "['x', 'y', 'pupil', 'baseline']" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "d = pp.get_example_data(\"rlmw_002_short\")\n", "d = d | pp.pupil.blinks_detect() | pp.pupil.blinks_interpolate() | pp.pupil.estimate_baseline()\n", "d.variables" ] }, { "cell_type": "markdown", "id": "89c5407c", "metadata": {}, "source": [ "## Debugging\n", "\n", "If you want to know exactly which steps `pypillometry` is taking, and which variables/eyes are being processed,\n", "you can use the `pp.loglevel()` context manager to temporarily increase the logging level (the result is a rather lengthy and detailed debug output):" ] }, { "cell_type": "code", "execution_count": 13, "id": "634b233a", "metadata": { "execution": { "iopub.execute_input": "2026-11-09T18:03:56.578207Z", "iopub.status.busy": "2026-11-09T18:03:56.577836Z", "iopub.status.idle": "2026-11-09T18:03:56.678450Z", "shell.execute_reply": "2026-11-09T18:03:56.677420Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m559\u001b[0m | \u001b[34m\u001b[1mchannels: left_pupil\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m584\u001b[0m | \u001b[34m\u001b[1mDetecting blinks for eye left\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mset_blinks\u001b[0m:\u001b[36m643\u001b[0m | \u001b[34m\u001b[1mchannels: left_pupil\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36m_convert_velocity_thresholds\u001b[0m:\u001b[36m454\u001b[0m | \u001b[34m\u001b[1mvel_onset -5.0/ms -> -10.0000/sample\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36m_convert_velocity_thresholds\u001b[0m:\u001b[36m463\u001b[0m | \u001b[34m\u001b[1mvel_offset 5.0/ms -> 10.0000/sample\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m332\u001b[0m | \u001b[34m\u001b[1mGenerated asymmetric velocity profiles, length 20465\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m336\u001b[0m | \u001b[34m\u001b[1mFound 147 potential onset points\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m346\u001b[0m | \u001b[34m\u001b[1mAfter filtering, 19 onsets remain\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m354\u001b[0m | \u001b[34m\u001b[1mFound 224 potential offset points\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m368\u001b[0m | \u001b[34m\u001b[1mAfter filtering, 36 offsets remain\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m392\u001b[0m | \u001b[34m\u001b[1mFound 13 blink pairs\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_velocity\u001b[0m:\u001b[36m403\u001b[0m | \u001b[34m\u001b[1mReturning 13 blinks after invalid-region adjustment\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m601\u001b[0m | \u001b[34m\u001b[1mDetected 13 blinks with velocity strategy\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_zero\u001b[0m:\u001b[36m438\u001b[0m | \u001b[34m\u001b[1mFound 11 potential blinks\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mdetect_blinks_zero\u001b[0m:\u001b[36m441\u001b[0m | \u001b[34m\u001b[1mFound 11 blinks after duration filtering\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m613\u001b[0m | \u001b[34m\u001b[1mDetected 11 blinks with zero strategy\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m618\u001b[0m | \u001b[34m\u001b[1mMerging 13 blinks with 11 blinks\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m620\u001b[0m | \u001b[34m\u001b[1mMerged 14 blinks\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mblinks_detect\u001b[0m:\u001b[36m632\u001b[0m | \u001b[34m\u001b[1mFiltered 14 blinks by minimum duration\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mpp: 19:03:56\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mset_blinks\u001b[0m:\u001b[36m643\u001b[0m | \u001b[34m\u001b[1mchannels: left_pupil\u001b[0m\n" ] } ], "source": [ "d = pp.get_example_data(\"rlmw_002_short\")\n", "with pp.loglevel(\"DEBUG\"):\n", " d.pupil.blinks_detect(eyes=\"left\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.15.1" } }, "nbformat": 4, "nbformat_minor": 5 }