Repository: aeye-lab/pymovements
Pull request #739 (2024-06-26 12:35:08)

feat: support 2kHz ASC files with floating-point timestamps
## Description

EyeLink's `edf2asc` CLI has an option `-ftime` that will output timestamps as floating point numbers. For 2000Hz recordings, this avoids issue #688, except that `parse_eyelink()` doesn't support floating-point timestamps. This PR adds this support.

Related to issue #688 (doesn't fix it, but might make it redundant?)

## Implemented changes

Insert a description of the changes implemented in the pull request.

- [x] Add floating point digits to regex patterns in `parsing.py`
- [x] Use Float64 dtype instead of Int64 for timestamps in DataFrames. (Note that for <= 1000Hz, the timestamps will still be [converted back to Int64](https://github.com/aeye-lab/pymovements/blob/58b18634caea74099333e6f4b8df1c8122fa455a/src/pymovements/gaze/gaze_dataframe.py#L1193-L1202) when calling `from_asc()`, so there is no change of behavior for `from_asc()` with other sampling rates.)
- [x] Necessary int→float conversions to make the type checker happy.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change is or requires a documentation update

## How Has This Been Tested?

- [x] Added a test case to `asc_test.py::test_from_asc_has_shape_and_schema` with a new test file `eyelink_monocular_2khz_example.asc` (created by hand)
- [x] Tested locally with a real ASC file generated with `edf2asc -ftime`

## 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/739?dropdown=coverage&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:
> Project coverage is 100.00%. Comparing base [(`58b1863`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/58b18634caea74099333e6f4b8df1c8122fa455a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) to head [(`4ec4955`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/4ec4955ab38f0b779f8619cb5fcc1d05600d412d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).

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


```diff
@@            Coverage Diff            @@
##              main      #739   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           60        60           
  Lines         2763      2763           
  Branches       715       715           
=========================================
  Hits          2763      2763           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/739?dropdown=coverage&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).

SiQube: hi @saeub thank you for the great contribution, just one minor comment above.



Patch:
diff --git a/src/pymovements/utils/parsing.py b/src/pymovements/utils/parsing.py
index 96072ca..f9a60fe 100755
--- a/src/pymovements/utils/parsing.py
+++ b/src/pymovements/utils/parsing.py
@@ -31,7 +31,7 @@ import numpy as np
 import polars as pl
 
 EYE_TRACKING_SAMPLE = re.compile(
-    r'(?P<time>(\d+))\s+'
+    r'(?P<time>(\d+[.]?\d*))\s+'
     r'(?P<x_pix>[-]?\d*[.]\d*)\s+'
     r'(?P<y_pix>[-]?\d*[.]\d*)\s+'
     r'(?P<pupil>\d*[.]\d*)\s+'
@@ -47,10 +47,10 @@ EYELINK_META_REGEXES = [
     },
     {'pattern': r'\*\*\s+(?P<version_2>EYELINK.*)'},
     {
-        'pattern': r'MSG\s+\d+\s+DISPLAY_COORDS\s+(?P<resolution>.*)',
+        'pattern': r'MSG\s+\d+[.]?\d*\s+DISPLAY_COORDS\s+(?P<resolution>.*)',
     },
     {
-        'pattern': r'MSG\s+\d+\s+RECCFG\s+(?P<tracking_mode>[A-Z,a-z]+)\s+'
+        'pattern': r'MSG\s+\d+[.]?\d*\s+RECCFG\s+(?P<tracking_mode>[A-Z,a-z]+)\s+'
                    r'(?P<sampling_rate>\d+)\s+'
                    r'(?P<file_sample_filter>(0|1|2))\s+'
                    r'(?P<link_sample_filter>(0|1|2))\s+'
@@ -60,12 +60,12 @@ EYELINK_META_REGEXES = [
         'pattern': r'PUPIL\s+(?P<pupil_data_type>(AREA|DIAMETER))\s*',
     },
     {
-        'pattern': r'MSG\s+\d+\s+ELCLCFG\s+(?P<mount_configuration>.*)',
+        'pattern': r'MSG\s+\d+[.]?\d*\s+ELCLCFG\s+(?P<mount_configuration>.*)',
     },
 ]
 
 VALIDATION_REGEX = (
-    r'MSG\s+(?P<timestamp>\d+)\s+!CAL\s+VALIDATION\s+HV'
+    r'MSG\s+(?P<timestamp>\d+[.]?\d*)\s+!CAL\s+VALIDATION\s+HV'
     r'(?P<num_points>\d\d?).*'
     r'(?P<tracked_eye>LEFT|RIGHT)\s+'
     r'(?P<error>\D*)\s+'
@@ -73,14 +73,14 @@ VALIDATION_REGEX = (
     r'(?P<validation_score_max>\d.\d\d)\s+max'
 )
 
-BLINK_START_REGEX = r'SBLINK\s+(R|L)\s+(?P<timestamp>(\d+))\s*'
+BLINK_START_REGEX = r'SBLINK\s+(R|L)\s+(?P<timestamp>(\d+[.]?\d*))\s*'
 BLINK_STOP_REGEX = (
-    r'EBLINK\s+(R|L)\s+(?P<timestamp_start>(\d+))\s+'
-    r'(?P<timestamp_end>(\d{6,10}))\s+(?P<duration_ms>(\d+))\s*'
+    r'EBLINK\s+(R|L)\s+(?P<timestamp_start>(\d+[.]?\d*))\s+'
+    r'(?P<timestamp_end>(\d+[.]?\d*))\s+(?P<duration_ms>(\d+[.]?\d*))\s*'
 )
-INVALID_SAMPLE_REGEX = r'(?P<timestamp>(\d{6,10}))\s+\.\s+\.\s+0\.0\s+0\.0\s+\.\.\.\s*'
+INVALID_SAMPLE_REGEX = r'(?P<timestamp>(\d+[.]?\d*))\s+\.\s+\.\s+0\.0\s+0\.0\s+\.\.\.\s*'
 
-CALIBRATION_TIMESTAMP_REGEX = r'MSG\s+(?P<timestamp>\d+)\s+!CAL\s*\n'
+CALIBRATION_TIMESTAMP_REGEX = r'MSG\s+(?P<timestamp>\d+[.]?\d*)\s+!CAL\s*\n'
 
 CALIBRATION_REGEX = (
     r'>+\s+CALIBRATION\s+\(HV(?P<num_points>\d\d?),'
@@ -88,9 +88,9 @@ CALIBRATION_REGEX = (
     r'(?P<tracked_eye>RIGHT|LEFT):\s+<{9}'
 )
 
-START_RECORDING_REGEX = r'START\s+(?P<timestamp>(\d+))\s+(RIGHT|LEFT)\s+(?P<types>.*)'
+START_RECORDING_REGEX = r'START\s+(?P<timestamp>(\d+[.]?\d*))\s+(RIGHT|LEFT)\s+(?P<types>.*)'
 STOP_RECORDING_REGEX = (
-    r'END\s+(?P<timestamp>(\d+))\s+\s+(?P<types>.*)\s+RES\s+'
+    r'END\s+(?P<timestamp>(\d+[.]?\d*))\s+\s+(?P<types>.*)\s+RES\s+'
     r'(?P<xres>[\d\.]*)\s+(?P<yres>[\d\.]*)\s*'
 )
 
@@ -128,7 +128,7 @@ def compile_patterns(patterns: list[dict[str, Any]]) -> list[dict[str, Any]]:
     list[dict[str, Any]]
         Returns from string compiled regex patterns.
     """
-    msg_prefix = r'MSG\s+\d+\s+'
+    msg_prefix = r'MSG\s+\d+[.]?\d*\s+'
 
     compiled_patterns = []
 
@@ -251,7 +251,7 @@ def parse_eyelink(
     blink = False
 
     start_recording_timestamp = ''
-    total_recording_duration = 0
+    total_recording_duration = 0.0
     num_blink_samples = 0
 
     for line in lines:
@@ -289,9 +289,9 @@ def parse_eyelink(
             blink = False
             parsed_blink = match.groupdict()
             blink_info = {
-                'start_timestamp': int(parsed_blink['timestamp_start']),
-                'stop_timestamp': int(parsed_blink['timestamp_end']),
-                'duration_ms': int(parsed_blink['duration_ms']),
+                'start_timestamp': float(parsed_blink['timestamp_start']),
+                'stop_timestamp': float(parsed_blink['timestamp_end']),
+                'duration_ms': float(parsed_blink['duration_ms']),
                 'num_samples': num_blink_samples,
             }
             num_blink_samples = 0
@@ -302,7 +302,7 @@ def parse_eyelink(
 
         elif match := compiled_recording_stop.match(line):
             stop_recording_timestamp = match.groupdict()['timestamp']
-            block_duration = int(stop_recording_timestamp) - int(start_recording_timestamp)
+            block_duration = float(stop_recording_timestamp) - float(start_recording_timestamp)
 
             total_recording_duration += block_duration
 
@@ -313,7 +313,7 @@ def parse_eyelink(
             y_pix_s = eye_tracking_sample_match.group('y_pix')
             pupil_s = eye_tracking_sample_match.group('pupil')
 
-            timestamp = int(timestamp_s)
+            timestamp = float(timestamp_s)
             x_pix = check_nan(x_pix_s)
             y_pix = check_nan(y_pix_s)
             pupil = check_nan(pupil_s)
@@ -372,7 +372,7 @@ def parse_eyelink(
         raise Warning('No metadata found. Please check the file for errors.')
 
     schema_overrides = {
-        'time': pl.Int64,
+        'time': pl.Float64,
         'x_pix': pl.Float64,
         'y_pix': pl.Float64,
         'pupil': pl.Float64,
@@ -440,7 +440,7 @@ def _calculate_data_loss(
         blinks: list[dict[str, Any]],
         invalid_samples: list[str],
         actual_num_samples: int,
-        total_rec_duration: int,
+        total_rec_duration: float,
         sampling_rate: float,
 ) -> tuple[float | str, float | str]:
     """Calculate data loss and blink loss.
@@ -453,7 +453,7 @@ def _calculate_data_loss(
         List of invalid samples.
     actual_num_samples: int
         Number of actual samples recorded.
-    total_rec_duration: int
+    total_rec_duration: float
         Total duration of the recording.
     sampling_rate: float
         Sampling rate of the eye tracker.


Test patch:
diff --git a/tests/files/eyelink_monocular_2khz_example.asc b/tests/files/eyelink_monocular_2khz_example.asc
new file mode 100644
index 0000000..4f5bbaf
--- /dev/null
+++ b/tests/files/eyelink_monocular_2khz_example.asc
@@ -0,0 +1,160 @@
+** CONVERTED FROM D:\pymovements\results\sub_1\sub_1.edf using edfapi 4.2.1 Win32  EyeLink Dataviewer Sub ComponentApr 12 2021 on Fri Mar 10 18:07:57 2023
+** DATE: Wed Mar  8 09:25:20 2023
+** TYPE: EDF_FILE BINARY EVENT SAMPLE TAGGED
+** VERSION: EYELINK II 1
+** SOURCE: EYELINK CL
+** EYELINK II CL v6.12 Feb  1 2018 (EyeLink Portable Duo)
+** CAMERA: EyeLink USBCAM Version 1.01
+** SERIAL NUMBER: CLU-DAB50
+** CAMERA_CONFIG: DAB50200.SCD
+** RECORDED BY pymovements
+** SREB2.2.299 WIN32 LID:20A87A96 Mod:2023.03.08 11:03 MEZ
+**
+
+MSG	2091650.0 !CMD 1 select_parser_configuration 0
+MSG	2091659.0 !CMD 0 fixation_update_interval = 50
+MSG	2091659.0 !CMD 0 fixation_update_accumulate = 50
+MSG	2091681.5 !CMD 1 auto_calibration_messages = YES
+MSG	2095865.0 DISPLAY_COORDS 0 0 1279 1023
+MSG	2095865.0 RETRACE_INTERVAL  16.646125144
+MSG	2095865.0 ENVIRONMENT   OpenGL on Windows (6, 2, 9200, 2, '')
+MSG	2095980.0 TRACKER_TIME 0 2095980.470
+MSG	2096367.0 -4 SYNCTIME 766 0
+MSG	2100624.0 SYNCTIME_READING
+INPUT	2117909.0	0
+INPUT	2126823.0	0
+MSG	2135819.5 !CAL
+>>>>>>> CALIBRATION (HV9,P-CR) FOR LEFT: <<<<<<<<<
+MSG	2135819.5 !CAL Calibration points:
+MSG	2135819.5 !CAL -32.6, -47.7        -0,    227
+MSG	2135819.5 !CAL -32.2, -63.6        -0,  -2267
+MSG	2135819.5 !CAL -32.6, -31.7        -0,   2624
+MSG	2135819.5 !CAL -58.0, -47.3     -3291,    227
+MSG	2135819.5 !CAL -7.7, -46.8      3291,    227
+MSG	2135820.0 !CAL -58.8, -65.2     -3358,  -2267
+MSG	2135820.0 !CAL -7.9, -61.4      3358,  -2267
+MSG	2135820.0 !CAL -55.5, -31.2     -3227,   2624
+MSG	2135820.0 !CAL -9.0, -31.3      3227,   2624
+MSG	2135820.0 !CAL  0.0,  0.0         0,      0
+MSG	2135820.0 !CAL eye check box: (L,R,T,B)
+	  -65     6   -72     7
+MSG	2135820.0 !CAL href cal range: (L,R,T,B)
+	-5037  5037 -3489  3847
+MSG	2135820.0 !CAL Cal coeff:(X=a+bx+cy+dxx+eyy,Y=f+gx+goaly+ixx+jyy)
+  -0.00043008  131.07  1.437  0.051949 -0.1007
+   227.35 -1.5024  153.47 -0.1679 -0.22845
+MSG	2135820.0 !CAL Prenormalize: offx, offy = -32.583 -47.715
+MSG	2135820.0 !CAL Quadrant center: centx, centy =
+  -0.00043025  227.35
+MSG	2135820.0 !CAL Corner correction:
+   9.5364e-06,  3.4194e-05
+  -1.6932e-05,  2.9132e-05
+   3.3933e-05,  3.5e-06
+   1.5902e-05,  8.6479e-06
+MSG	2135820.0 !CAL Gains: cx:152.074 lx:170.107 rx:152.936
+MSG	2135820.0 !CAL Gains: cy:128.550 ty:155.848 by:116.611
+MSG	2135820.0 !CAL Resolution (upd) at screen center: X=1.7, Y=2.0
+MSG	2135820.0 !CAL Gain Change Proportion: X: 0.112 Y: 0.336
+MSG	2135821.0 !CAL Gain Ratio (Gy/Gx) = 0.845
+MSG	2135821.0 !CAL Bad Y/X gain ratio: 0.845
+MSG	2135821.0 !CAL PCR gain ratio(x,y) = 2.507, 2.179
+MSG	2135821.0 !CAL CR gain match(x,y) = 1.010, 1.010
+MSG	2135821.0 !CAL Slip rotation correction OFF
+MSG	2135821.0 !CAL CALIBRATION HV9 L LEFT    GOOD
+INPUT	2137650.0	0
+MSG	2148587.0 !CAL VALIDATION HV9 L LEFT  GOOD ERROR 0.27 avg. 0.83 max  OFFSET 0.11 deg. 3.7,2.4 pix.
+MSG	2148587.0 VALIDATE L POINT 0  LEFT  at 640,512  OFFSET 0.19 deg.  7.2,1.0 pix.
+MSG	2148587.0 VALIDATE L POINT 1  LEFT  at 640,159  OFFSET 0.12 deg.  3.9,-2.2 pix.
+MSG	2148587.0 VALIDATE L POINT 2  LEFT  at 640,864  OFFSET 0.42 deg.  -15.8,0.9 pix.
+MSG	2148587.0 VALIDATE L POINT 3  LEFT  at 172,512  OFFSET 0.83 deg.  26.3,17.5 pix.
+MSG	2148587.0 VALIDATE L POINT 4  LEFT  at 1107,512  OFFSET 0.19 deg.  -4.3,5.7 pix.
+MSG	2148587.0 VALIDATE L POINT 5  LEFT  at 228,201  OFFSET 0.06 deg.  1.3,1.9 pix.
+MSG	2148587.0 VALIDATE L POINT 6  LEFT  at 1051,201  OFFSET 0.33 deg.  -3.0,-12.5 pix.
+MSG	2148587.0 VALIDATE L POINT 7  LEFT  at 228,822  OFFSET 0.18 deg.  -6.8,0.2 pix.
+MSG	2148587.0 VALIDATE L POINT 8  LEFT  at 1051,822  OFFSET 0.18 deg.  3.8,5.5 pix.
+INPUT	2153108.0	0
+MSG	2154447.0 DRIFTCORRECT L LEFT  at 133,133  OFFSET 0.38 deg.  12.5,7.9 pix.
+MSG	2154540.0 TRIALID 0
+MSG	2154555.5 RECCFG CR 2000 2 1 L
+MSG	2154555.5 ELCLCFG BTABLER
+MSG	2154555.5 GAZE_COORDS 0.00 0.00 1279.00 1023.00
+MSG	2154555.5 THRESHOLDS L 102 242
+MSG	2154555.5 ELCL_WINDOW_SIZES 176 188 0 0
+MSG	2154555.5 CAMERA_LENS_FOCAL_LENGTH 27.00
+MSG	2154555.5 PUPIL_DATA_TYPE RAW_AUTOSLIP
+MSG	2154555.5 ELCL_PROC CENTROID (3)
+MSG	2154555.5 ELCL_PCR_PARAM 5 3.0
+START	2154556.0 	LEFT	SAMPLES	EVENTS
+PRESCALER	1
+VPRESCALER	1
+PUPIL	AREA
+EVENTS	GAZE	LEFT	RATE	2000.00	TRACKING	CR	FILTER	2
+SAMPLES	GAZE	LEFT	RATE	2000.00	TRACKING	CR	FILTER	2	INPUT
+INPUT	2154556.0	0
+MSG	2154556.0 !MODE RECORD CR 2000 2 1 L
+2154556.5	  138.1	  132.8	  778.0	    0.0	...
+MSG	2154598.0 SYNCTIME_READING_SCREEN_0
+2154557.0	  138.2	  132.7	  778.0	    0.0	...
+MSG	2154598.0 SYNCTIME_READING_SCREEN_1
+SFIX L   2154563.0
+2154560.5	  137.9	  131.6	  777.0	    0.0	...
+MSG 2154562.0 READING.STOP
+MSG	2154562.0 SYNCTIME_JUDO
+MSG 2154562.0 SYNCTIME.P0
+MSG 2154562.0 TRIALID 1
+2154564.0	  138.1	  131.0	  778.0	    0.0	...
+MSG 2154566.0 P0.STOP
+MSG 2154566.0 SYNCTIME.P1
+2154596.0	  139.6	  132.1	  784.0	    0.0	...
+MSG 2154597.0 P1.STOP
+MSG 2154597.0 SYNCTIME.P2
+2154598.5	  139.5	  131.9	  784.0	    0.0	...
+MSG	2154599.0 -11 !V DRAW_LIST ../../runtime/dataviewer/sub_1/graphics/VC_1.vcl
+MSG 2154599.0 P2.STOP
+MSG 2154599.0 SYNCTIME.P3
+2154599.0	  139.5	  131.8	  784.0	    0.0	...
+MSG 2154694.0 P3.STOP
+MSG 2154694.0 TRIALID 2
+2154695.0	  147.2	  134.4	  798.0	    0.0	...
+MSG 2154562.0 SYNCTIME.P0
+EFIX L   2154563.0	2154695.0	133	  141.0	  132.2	    791
+SSACC L  2154696.0
+2154696.0	  147.3	  134.1	  799.0	    0.0	...
+SSACC L  2339227.0
+MSG 2339225.0 P0.STOP
+MSG 2339225.0 TRIALID 3
+MSG 2339225.0 SYNCTIME.P0
+2339227.0	  673.2	  523.8	  619.0	    0.0	...
+2339245.0	  629.0	  531.4	  621.0	    0.0	...
+ESACC L  2339227.0	2339245.0	19	  673.2	  523.8	  629.0	  531.4	   1.21	    103
+SFIX L   2339246.0
+MSG 2339246.0 P0.STOP
+MSG 2339246.0 SYNCTIME.P1
+2339246.0	  629.9	  531.9	  622.0	    0.0	...
+MSG 2339271.0 P1.STOP
+MSG 2339271.0 SYNCTIME.P2
+2339271.5	  639.4	  531.9	  617.0	    0.0	...
+MSG 2154560.0 TRIALID 4
+MSG 2339272.0 SYNCTIME.P0
+2339272.0	  639.0	  531.9	  617.0	    0.0	...
+MSG 2339290.0 P0.STOP
+MSG 2339290.0 SYNCTIME.P1
+2339290.0	  637.6	  531.4	  618.0	    0.0	...
+EFIX L   2339246.0	2339290.0	45	  638.5	  531.8	    618
+MSG 2339291.0 P1.STOP
+MSG 2339291.0 READING.STOP
+MSG 2339291.0 TRIAL_RESULT
+2339291.0	  637.3	  531.2	  618.0	    0.0	...
+END	2339292 	SAMPLES	EVENTS	RES	  37.17	  37.75
+MSG	2339832.0 !V TRIAL_VAR Session_Name_ sub_1
+MSG	2339833.0 !V TRIAL_VAR Trial_Index_ 13
+MSG	2339839.0 !V TRIAL_VAR TRIAL_TYPE -1
+MSG	2339840.0 !V TRIAL_VAR CONTROL_FLOW [1, 2, 1, 4, 2, 1, 3, 1, 2, 1, 5]
+MSG	2339841.0 !V TRIAL_VAR CONTROL_FLOW_INDEX -1
+MSG	2339895.0 !V TRIAL_VAR SESSION_ID -1
+MSG	2339896.0 !V TRIAL_VAR SESSION_TYPE "start"
+MSG	2339897.0 !V TRIAL_VAR SUBJECT_ID -1
+MSG	2339898.0 !V TRIAL_VAR SCREEN_ID 0
+MSG	2339899.0 !V TRIAL_VAR STIMULUS_COMBINATION_ID start
+MSG	2339900.0 !V TRIAL_VAR VIDEO_SERVER_MESSAGE none
+MSG	2339985.0 TRIAL_RESULT 0
diff --git a/tests/unit/gaze/io/asc_test.py b/tests/unit/gaze/io/asc_test.py
index 1e582c9..c5fb157 100644
--- a/tests/unit/gaze/io/asc_test.py
+++ b/tests/unit/gaze/io/asc_test.py
@@ -97,6 +97,37 @@ import pymovements as pm
             ),
             id='eyelink_asc_mono_pattern_list',
         ),
+        pytest.param(
+            {
+                'file': 'tests/files/eyelink_monocular_2khz_example.asc',
+                'patterns': 'eyelink',
+            },
+            pl.from_dict(
+                data={
+                    'time': [
+                        2154556.5, 2154557.0, 2154560.5, 2154564.0, 2154596.0, 2154598.5, 2154599.0,
+                        2154695.0, 2154696.0, 2339227.0, 2339245.0, 2339246.0, 2339271.5, 2339272.0,
+                        2339290.0, 2339291.0,
+                    ],
+                    'pupil': [
+                        778.0, 778.0, 777.0, 778.0, 784.0, 784.0, 784.0, 798.0,
+                        799.0, 619.0, 621.0, 622.0, 617.0, 617.0, 618.0, 618.0,
+                    ],
+                    'pixel': [
+                        [138.1, 132.8], [138.2, 132.7], [137.9, 131.6], [138.1, 131.0],
+                        [139.6, 132.1], [139.5, 131.9], [139.5, 131.8], [147.2, 134.4],
+                        [147.3, 134.1], [673.2, 523.8], [629.0, 531.4], [629.9, 531.9],
+                        [639.4, 531.9], [639.0, 531.9], [637.6, 531.4], [637.3, 531.2],
+                    ],
+                },
+                schema={
+                    'time': pl.Float64,
+                    'pupil': pl.Float64,
+                    'pixel': pl.List(pl.Float64),
+                },
+            ),
+            id='eyelink_asc_mono_2khz_pattern_eyelink',
+        ),
     ],
 )
 def test_from_asc_has_shape_and_schema(kwargs, expected_frame):
diff --git a/tests/unit/utils/parsing_test.py b/tests/unit/utils/parsing_test.py
index d3bbc65..0b8347b 100644
--- a/tests/unit/utils/parsing_test.py
+++ b/tests/unit/utils/parsing_test.py
@@ -128,8 +128,8 @@ PATTERNS = [
 EXPECTED_DF = pl.from_dict(
     {
         'time': [
-            10000000, 10000002, 10000004, 10000006, 10000008, 10000011, 10000014,
-            10000017, 10000019, 10000020, 10000021,
+            10000000.0, 10000002.0, 10000004.0, 10000006.0, 10000008.0, 10000011.0, 10000014.0,
+            10000017.0, 10000019.0, 10000020.0, 10000021.0,
         ],
         'x_pix': [850.7, 850.7, 850.7, 850.7, 850.7, 850.7, 850.7, 850.7, np.nan, np.nan, np.nan],
         'y_pix': [717.5, 717.5, 717.5, 717.5, 717.5, 717.5, 717.5, 717.5, np.nan, np.nan, np.nan],