# Synthetic Gazepoint pupillometry case study

## Purpose

This case study is entirely synthetic. Its statistics are software demonstrations and are not empirical evidence about Gazepoint hardware, participants, or psychological processes.

    sim <- simulate_pupil_timecourse(
      n_participants = 8,
      trials_per_participant = 6,
      sampling_frequency = 20,
      time_window = c(-0.5, 1.5),
      condition_difference = 0.18,
      blink_trial_probability = 0.04,
      include_gaze = TRUE,
      include_luminance = TRUE,
      seed = 20260814
    )

## A Gazepoint-like mapping audit

The raw simulator is vendor-neutral. The next object creates a small Gazepoint-like view solely to exercise the verified field bridge.

    gp_like <- data.frame(
      TIME = sim$data$event_time[1:20],
      LPD = 15 + sim$data$pupil_mm[1:20],
      LPV = as.integer(!is.na(sim$data$pupil_mm[1:20])),
      BPOGX = sim$data$gaze_x[1:20],
      BPOGY = sim$data$gaze_y[1:20],
      BPOGV = 1
    )
    gazepoint_pupil_mapping_table(inspect_gazepoint_pupil_schema(gp_like))
    #>        field                        role              unit      eye
    #> 1       TIME                        time           seconds     none
    #> 2  TIME_TICK                   time_tick             ticks     none
    #> 3      LPOGX                 left_gaze_x normalized_screen     left
    #> 4      LPOGY                 left_gaze_y normalized_screen     left
    #> 5      LPOGV             left_gaze_valid normalized_screen     left
    #> 6      RPOGX                right_gaze_x normalized_screen    right
    #> 7      RPOGY                right_gaze_y normalized_screen    right
    #> 8      RPOGV            right_gaze_valid normalized_screen    right
    #> 9      BPOGX                 best_gaze_x normalized_screen combined
    #> 10     BPOGY                 best_gaze_y normalized_screen combined
    #> 11     BPOGV             best_gaze_valid normalized_screen combined
    #> 12      LPCX         left_pupil_camera_x     camera_pixels     left
    #> 13      LPCY         left_pupil_camera_y     camera_pixels     left
    #> 14       LPD  left_pupil_diameter_pixels            pixels     left
    #> 15       LPS            left_pupil_scale             scale     left
    #> 16       LPV            left_pupil_valid         indicator     left
    #> 17      RPCX        right_pupil_camera_x     camera_pixels    right
    #> 18      RPCY        right_pupil_camera_y     camera_pixels    right
    #> 19       RPD right_pupil_diameter_pixels            pixels    right
    #> 20       RPS           right_pupil_scale             scale    right
    #> 21       RPV           right_pupil_valid         indicator    right
    #> 22     LEYEX                  left_eye_x            metres     left
    #> 23     LEYEY                  left_eye_y            metres     left
    #> 24     LEYEZ                  left_eye_z            metres     left
    #> 25   LPUPILD  left_pupil_diameter_metres            metres     left
    #> 26   LPUPILV         left_pupil_3d_valid         indicator     left
    #> 27     REYEX                 right_eye_x            metres    right
    #> 28     REYEY                 right_eye_y            metres    right
    #> 29     REYEZ                 right_eye_z            metres    right
    #> 30   RPUPILD right_pupil_diameter_metres            metres    right
    #> 31   RPUPILV        right_pupil_3d_valid         indicator    right
    #>                                  source_specification present
    #> 1  Gazepoint Open Gaze API v2-era field specification    TRUE
    #> 2  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 3  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 4  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 5  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 6  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 7  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 8  Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 9  Gazepoint Open Gaze API v2-era field specification    TRUE
    #> 10 Gazepoint Open Gaze API v2-era field specification    TRUE
    #> 11 Gazepoint Open Gaze API v2-era field specification    TRUE
    #> 12 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 13 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 14 Gazepoint Open Gaze API v2-era field specification    TRUE
    #> 15 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 16 Gazepoint Open Gaze API v2-era field specification    TRUE
    #> 17 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 18 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 19 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 20 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 21 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 22 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 23 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 24 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 25 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 26 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 27 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 28 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 29 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 30 Gazepoint Open Gaze API v2-era field specification   FALSE
    #> 31 Gazepoint Open Gaze API v2-era field specification   FALSE

`LPD` is labelled as pixels by the bridge. The example does not convert those synthetic values into millimetres.

## Contract through specification

    contract <- create_pupil_contract(
      outcome_col = "pupil_mm",
      participant_col = "participant_id",
      trial_col = "trial_id",
      item_col = "item_id",
      condition_col = "condition",
      time_col = "event_time",
      pupil_unit = "millimetres",
      sampling_frequency = 20,
      eye = "combined",
      blink_col = "blink",
      interpolation_col = "interpolated",
      gaze_x_col = "gaze_x",
      gaze_y_col = "gaze_y",
      luminance_col = "luminance",
      baseline_window = c(-0.5, 0),
      preprocessing_provenance = "gp3bayes deterministic simulator"
    )
    prepared <- prepare_pupil_timecourse(sim$data, contract)
    readiness <- audit_pupil_readiness(prepared)
    measurement <- audit_pupil_measurement_context(prepared)
    spec <- specify_pupil_timecourse_model(
      prepared,
      smooth_basis_dimension = 6,
      autocorrelation = "ar1"
    )
    pupil_readiness_table(readiness)
    #>                                   metric       value status
    #> 1                                   rows        1968   pass
    #> 2                           participants           8   pass
    #> 3                                 trials          48   pass
    #> 4                                  items          12   pass
    #> 5                             conditions           2   pass
    #> 6                  estimated_sampling_hz          20   pass
    #> 7               median_sampling_interval        0.05   pass
    #> 8                   sampling_interval_cv 1.38814e-15   pass
    #> 9               missing_pupil_proportion           0   pass
    #> 10                      blink_proportion           0   pass
    #> 11               interpolated_proportion           0   pass
    #> 12                    invalid_proportion        <NA> review
    #> 13                     baseline_coverage           1   pass
    #> 14               trials_lacking_baseline           0   pass
    #> 15                             pupil_min     3.18649   pass
    #> 16                             pupil_max     5.30599   pass
    #> 17               minimum_trial_time_span           2 review
    #> 18               maximum_trial_time_span           2 review
    #> 19                        gaze_available        TRUE   pass
    #> 20              gaze_condition_imbalance  0.00178244 review
    #> 21         left_right_pupil_disagreement        <NA> review
    #> 22                   luminance_available        TRUE   pass
    #> 23         luminance_condition_imbalance 3.24082e-05 review
    #> 24                    contrast_available       FALSE review
    #> 25                pfe_corrected_upstream       FALSE review
    #> 26 preprocessing_provenance_completeness       0.333 review
    #>                                                                       detail
    #> 1
    #> 2  One-participant data cannot support population participant heterogeneity.
    #> 3
    #> 4                                                Item hierarchy is optional.
    #> 5                         A condition contrast requires at least two levels.
    #> 6                                                            Declared 20 Hz.
    #> 7
    #> 8            AR(1) sample-order dependence requires regular-enough sampling.
    #> 9           Missingness is reported, not automatically repaired or excluded.
    #> 10
    #> 11
    #> 12
    #> 13
    #> 14
    #> 15
    #> 16
    #> 17                       Smallest observed within-trial event-time coverage.
    #> 18                        Largest observed within-trial event-time coverage.
    #> 19
    #> 20                   Descriptive between-condition gaze-position difference.
    #> 21                                Median absolute paired-channel difference.
    #> 22
    #> 23                  Descriptive between-condition mean luminance difference.
    #> 24
    #> 25           PFE status is contextual evidence, not an automatic correction.
    #> 26
    pupil_measurement_audit_table(measurement)
    #>          domain available   observed
    #> 1         blink      TRUE          0
    #> 2 interpolation      TRUE          0
    #> 3      baseline      TRUE  -0.5 to 0
    #> 4      sampling      TRUE 1.3881e-15
    #> 5 gaze_position      TRUE  0.0031145
    #> 6           pfe      TRUE      FALSE
    #> 7     luminance      TRUE 3.2408e-05
    #> 8      contrast     FALSE       <NA>
    #> 9  time_on_task     FALSE       <NA>
    #>                                                       interpretation
    #> 1                                   Reported data-loss context only.
    #> 2                      Reported upstream interpolation context only.
    #> 3                         Baseline declaration; no automatic choice.
    #> 4              Sampling irregularity context for temporal modelling.
    #> 5 Between-condition mean gaze-position difference; descriptive only.
    #> 6  Upstream PFE-correction declaration; no automatic PFE correction.
    #> 7     Between-condition mean luminance difference; descriptive only.
    #> 8              Contrast availability/range; no automatic adjustment.
    #> 9                             Recording-time support when available.
    pupil_specification_table(spec)
    #>                     field
    #> 1                  family
    #> 2              likelihood
    #> 3                    link
    #> 4                 formula
    #> 5      temporal_structure
    #> 6  smooth_basis_dimension
    #> 7    condition_trajectory
    #> 8         autocorrelation
    #> 9     participant_effects
    #> 10 participant_trajectory
    #> 11           item_effects
    #> 12             covariates
    #> 13           outcome_unit
    #> 14     baseline_operation
    #> 15   unrestricted_formula
    #>                                                                                                                                                                 value
    #> 1                                                                                                                                                               pupil
    #> 2                                                                                                                                                            Gaussian
    #> 3                                                                                                                                                            identity
    #> 4  .pupil_model ~ .condition + s(.event_time, by = .condition, k = 6) +      (1 | .participant) + (1 | .item) + ar(time = .sample_index,      gr = .series_id, p = 1)
    #> 5                                                                                                                                                              smooth
    #> 6                                                                                                                                                                   6
    #> 7                                                                                                                                                                TRUE
    #> 8                                                                                                                                                                 ar1
    #> 9                                                                                                                                                    random_intercept
    #> 10                                                                                                                                                               none
    #> 11                                                                                                                                                               TRUE
    #> 12
    #> 13                                                                                                                                                        millimetres
    #> 14                                                                                                                                                               none
    #> 15                                                                                                                                                              FALSE

## Backend and post-fit stages

    fit <- fit_pupil_model_backend(spec, backend = "cmdstanr", cores = 2)
    trajectory <- estimate_pupil_trajectory(
      predict_pupil_trajectory(fit, ndraws = 200)
    )
    window <- estimate_pupil_window(
      predict_pupil_trajectory(fit, ndraws = 200),
      window = c(0.3, 1.0)
    )
    auc <- estimate_pupil_auc(
      predict_pupil_trajectory(fit, ndraws = 200),
      window = c(0.3, 1.0)
    )
    ppc <- check_pupil_posterior_predictive(fit, ndraws = 200)
    diag <- diagnose_pupil_fit(fit)

    plan <- create_pupil_validation_plan(
      prepared,
      target = "new_trial_known_participant",
      K = 4
    )
    validation <- validate_pupil_model(fit, plan, execute = TRUE)

## Sensitivity

    spec_for_sensitivity <- specify_pupil_timecourse_model(
      prepared,
      autocorrelation = "none",
      smooth_basis_dimension = 5
    )
    suite <- create_pupil_sensitivity_suite(
      spec_for_sensitivity,
      baseline_windows = list(c(-0.5, -0.1), c(-0.4, -0.1)),
      baseline_window_operation = "subtract",
      interpolation_policy = c("retain", "exclude_flagged"),
      gaze_adjustment = c("none", "declared_covariates"),
      luminance_adjustment = c("none", "declared_covariate"),
      analysis_windows = list(c(0.3, 1.0))
    )
    head(pupil_sensitivity_table(suite))
    #>   scenario_id                 axis               value
    #> 1        S001      baseline_window           -0.5,-0.1
    #> 2        S002      baseline_window           -0.4,-0.1
    #> 3        S003 interpolation_policy              retain
    #> 4        S004 interpolation_policy     exclude_flagged
    #> 5        S005      gaze_adjustment                none
    #> 6        S006      gaze_adjustment declared_covariates

All windows and sensitivity dimensions are declared. No simulated estimate is presented as an empirical effect, and no scenario is automatically selected.