--- title: "GazeR preprocssing for SR Reseerch Trackers based on ASC files" author: "Holger Mitterer and Jason Geller" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{"Gazer and ASC"} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` # The Issue The gazeR package relied on the edfR package for data from SR Research Eyetrackers. The edfR package, however, is only available for Linux and Mac systems, and even there has been running into problems. Therefore, the current functions emulate the workings of these functions using the *.asc files that can be generated from edf files using either the visual EDF2ASC tool that is distributed with each SR tracker or the command line version of EDF2ASC. These functions handle both visual-world-paradigm (gaze) and pupillometry data, and support monocular as well as binocular recordings (see the binocular option below). **New in gazeR 0.2.5:** `read_asc()` reads a single `.asc` file straight into a data frame, with messages, trial variables and blink flags already attached. It is much faster than the `parse_asc()` workflow below and needs no folder-per-participant setup, so it is now the recommended way to read `.asc` files. See [Reading ASC files with read_asc()](#reading-asc-files-with-read_asc) below. # EDF to ASC conversion It is critical that the EDF conversion puts out both events and samples!! If the user copies the typical structure of an SR projects, the individual participants' data files be organized in their respective directories, and this model is followed here. The first function generates a batch file that allows the one-click generation of the asc files using the command line conversion tool in Windows. **For this to work you must download the edf2asc tool from SR--this is free.** It requires a main directory that contains all participants directories and a list of those directories. The user can then navigate with the command prompt to the output directory (provided in the output) and run the batch file. This conversion will take quite long then! ```{r, message=FALSE, warning=FALSE, eval=FALSE} remotes::install_github("dmirman/gazer") library(gazer) library(here) library(data.table) library(tidyverse) library(knitr) ``` ```{r, message=FALSE, warning=FALSE, echo=FALSE, eval=FALSE} #a list of the subdirectories generated with a regular expression #starts with "pp" followed by one or more digits before the end of the string #matches pp1, pp09, pp1347, etc. #if your data directories are called "subject1", "subject2", etc.\ #use "^subject\\d+$") mainDir=here::here() subDirs = dir(mainDir, pattern = "^pp\\d+$") setwd(mainDir) cat(paste(subDirs, collapse = "\n")) ``` ```{r, eval=FALSE} generate_edf2asc_bat(mainDir, subDirs) ``` # Reading ASC files with read_asc() {#reading-asc-files-with-read_asc} `read_asc()` reads one `.asc` file (gzipped or not) and returns one row per sample. gazeR includes an example: the first 6 trials of one participant (PN010) from a Black-and-White Switch task in the public OSF project [Dynamics of the Pupillary Consensual Response](https://osf.io/7k9tr/), recorded binocularly at 1000 Hz. ```{r, message=FALSE, warning=FALSE} library(gazer) library(dplyr) asc_file <- system.file("extdata", "PN010BWS.asc.gz", package = "gazer") pn010 <- read_asc(asc_file, eye = "left") pn010 ``` The columns are: - `subject`: the file name without extension (or set `subject =`) - `trial`: a trial runs from each `TRIALID` message to its `TRIAL_RESULT` message - `time`: ms from the start of recording in that trial (the `!MODE RECORD` message), as in `parse_asc()` - `pupil`, `x`, `y`: `NA` when the tracker lost the eye (instead of `0` and `1e8`) - `blink`: 1 during a blink, from the tracker's `SBLINK`/`EBLINK` events - `message`: event messages, each attached to the first sample at or after it, with Experiment Builder offsets applied - `tracker_time`: the original tracker clock - one column per Experiment Builder trial variable (`!V TRIAL_VAR`) The sampling rate, recorded eyes and pupil type are stored with the data: ```{r} attr(pn010, "asc_info") ``` Trial variables and messages are ready to use, with no separate `find_messages_asc()` or merge step: ```{r} pn010 |> distinct(trial, block, screen) pn010 |> filter(!is.na(message)) |> select(trial, time, message) ``` Messages logged before recording starts in each trial (`TRIALID` and the tracker's setup messages) have no sample to attach to. `keep_messages = TRUE` returns every message, with its time relative to the start of recording: ```{r} msgs <- attr(read_asc(asc_file, keep_messages = TRUE), "messages") msgs |> filter(trial == 1, time <= 0) ``` ## Binocular recordings with read_asc() For binocular files, `eye` chooses `"left"`, `"right"`, `"average"` (the default), or `"both"`, which keeps `pupil_left`, `pupil_right`, `x_left`, ..., `blink_left` and `blink_right` alongside the average. With `"average"` and `"both"`: - `pupil` (and `x`, `y`) is the **monocular mean** of the two eyes (`compute_monocular_mean()`), as EyeLink reports the average eye: the mean of both eyes, or the value of the one valid eye. It is `NA` only when neither eye is valid. - `blink` is 1 if **either** eye is in a blink (`blink_left | blink_right`). If you treat blink samples as missing, this also removes samples where `pupil` comes from the one eye that was still open. If you analyse one eye, use that eye's columns together, e.g. `pupil_left` with `blink_left`. ```{r} read_asc(asc_file, eye = "both") |> select(trial, time, pupil, pupil_left, pupil_right, blink_left, blink_right) |> head() ``` ## Several participants Read each file and stack the results. Keep each participant's sampling rate and check that they match: blink extension, interpolation gaps and filtering all depend on it, so participants recorded at different rates (e.g. 500 vs 1000 Hz) should be processed separately. ```{r, eval=FALSE} files <- list.files("data/asc", pattern = "\\.asc$", full.names = TRUE) all_data <- lapply(files, function(f) { d <- read_asc(f, eye = "left") d$hz <- attr(d, "asc_info")$hz d }) |> bind_rows() all_data |> distinct(subject, hz) ``` At 500 Hz a sample arrives only every 2 ms, so a trial's first sample is at 0 or 1 ms after recording starts, roughly half the time each. This is expected. The output of `read_asc()` can go straight into the pupil functions (`extend_blinks()`, `smooth_interpolate_pupil()`, `baseline_correction_pupil_msg()`, ...); see the pupil pre-processing vignette. [shiny gazeR](https://github.com/jgeller112/shiny-gazeR) provides a point-and-click interface that reads `.asc` files with `read_asc()`. # Parsing the ASC File This function is quite analogous to the `parse_edf` function in gazeR. As an additional convenience it will generate a list of the type of messages that are found in the asc file to guide the extraction of performance and trial-level data (i.e., where is the target located on the screen, what is the condition of the trial, etc.) The parse_asc function requires a directory list as input, and overs the additional option of overwriting missing data from blinks with the last available data point (as in Mitterer and McQueen, 2009, Journal of Experimental Psychology: Human Percpetion and Performance). This option is set by default to false. Moreover, the function offers an additional parameter to keep the file sizes small (and the subsequent processing faster). The user can opt to not write out scene preview time with the parameter cut preview. If that parameter is set to a time value (in milliseconds), the output will start after the preview time. **Note that the home directory needs to be specified including a final slash** ```{r, eval=FALSE} parse_asc(myDirList, homeDir = "C:/Users/JG1890/Documents/gazer/" , overwriteBlinks = F, cutPreview = 1500) ``` ## Binocular recordings By default `parse_asc` returns a single (monocular) eye with the columns `time, trial, ID, subject, pupil, x, y`. If your recording tracked **both** eyes, set `binocular = TRUE`. The output then contains separate x/y/pupil columns for each eye plus a per-eye blink flag (0/1): `time, trial, ID, subject, x_left, y_left, pupil_left, x_right, y_right, pupil_right, blink_left, blink_right` The blink flags are derived from the `SBLINK`/`EBLINK` events in the asc file and mark, sample by sample, whether each eye was mid-blink. ```{r, eval=FALSE} parse_asc(myDirList, homeDir = "C:/Users/JG1890/Documents/gazer/", overwriteBlinks = F, cutPreview = 1500, binocular = TRUE) ``` This function will take quite long to complete. However, it will feed back whenever it has finished processing a trial from a given participants (which may take around 1s, depending on sample size and the machine the program is run on). That is, for a study with 40 participants and 160 trials, this may well take more than 100 minutes. The function generates three files in the home directory: 1. INPUTFILE_eye.csv containing the eye-tracking data 2. overview_INPUTFILE.txt containing an overview of the messages in the asc file 3. VAR_overview_INPUTFILE.txt containing an overview of the TRIAL_VARIABLES that have been put out while running the experiment. **Known issue:** `parse_asc()` treats any line whose first field is a whole number as a sample. Calibration-result lines in the file header (e.g. ` 1 1 1 1094138081`) match this, so they end up as fake samples in trial 1. When several participants are processed in one call, these rows get a large negative `time` carried over from the previous participant, and two or more of them can share the same time. In existing `_eye.csv` and `_combined.csv` files, these are the rows with `time < 0` and can be deleted. `read_asc()` only reads lines that start with a timestamp, so it is not affected. # Extraction of messages The next step is to extract information about the trials from the messages. This will be specific to each study, but the overview files should help to remind the user of how the experiment was set up. This will be achieved with the `find_messages_asc` function, which is similar to the `find_messages_edf` function working on edf files. However, it has a few more options to facilitate further processing. It has four parameters: 1. `myDirList` containing a list of directories to work on 2. `time2extract`: a list of strings that match message texts for which only the time should be retained. This is useful when messages have been sent at critical time points (such as the onset/offset of a target word) and times need to be normalized in relation to this time point (analogue to markers generated from the stimuli-providing machine send to the machine that records the EEG data). That is, only the time will be retained, but is therefore immediately available as number 3. `msg2extract`: A list of messages to extract fully based on the first word of that message (output will be string variables) 4. `vars2extrac`: A list of Trial variables used by Experiment Builder that should be extracted (value only, with column names set by the variable name) ```{r, eval=FALSE} myDirList = dir(mainDir,pattern = "^pp01$") #the three types of information to be extracted msg2extracts = c("clicked") time2extracts = c("TargetOnset") vars2extracts = c("targetpos", "targetw","competpos", "competw","condition", "item","trialtype", "wav") find_messages_asc(myDirList, homeDir = "C:/Users/JG1890/Documents/gazer/", time2extract = time2extracts, msg2extract = msg2extracts, vars2extract = vars2extracts) ``` # Merging Eye Data and Behavioral Data The next step is analogous to the `merge_gazer_files` function, which merges messages with the eye-tracking data. The function is called `merge_asc_files` and simply required one parameter, the list of directories in which to find files. For each directory, the function will feedback whether it found the expected files, and also produce a summary of how many merge attempts were succesful or not. In the example below, I purposefully included some entries in the directory list for which things dont work, so that the error handling becomes clear. ```{r, eval=FALSE} myDirList = dir(mainDir, pattern = "^pp") merge_asc_files(myDirList,"C:/Users/JG1890/Documents/gazer/" ) ``` # Eye-tracking analysis The files resulting from this process can be used with the general gazeR function, as explained in other vignettes. The code below how to read in data from multiple participants to process them further using gazeR. Below is the data ready for analysis in gazeR. ```{r, eval=FALSE} gaze_dat <- fread(here::here("pp3", "pp3_combined.csv")) %>% as_tibble() summary(gaze_dat) ```