Changelog
Source:NEWS.md
eyesim (development version)
New features
-
fixation_revisits()scores spatial returns to locations already fixated in the same episode, related to Kragel et al. (2021). It does not implement the backtracking-sequence classification of Voss et al. (2011). It returns one row per fixation with the opportunity, revisit, lag and matched fixation.revisit_summary()gives counts and rates per episode. - A revisit must reach at least
min_lagfixations back (default 2) and, by default, arrive from a preceding fixation at leastradiusaway. This incoming-step rule does not establish departure from the original region. - An optional spatial null,
reference, gives the expected revisit rate and the excess over it. Without it, revisit counts largely restate fixation number and dispersion. - The
eye_tablemethod summarises every episode. Withnull = "subject"it builds the null from the same subject’s other episodes (leave-episode-out). Withnull = "item"it uses other viewers of the same image (item_var). Whensubject_varis also given, all of the same subject’s episodes are excluded. - Re-scoring an
eye_tablethatfixation_revisits()already scored replaces only the columns the earlier call wrote. A column of the same name added by the caller (for example the tracker’s ownn_fix) is an error and is never silently overwritten. -
visual_angle_to_px()andpx_to_visual_angle()convert between degrees of visual angle and screen pixels, so a revisitradiuscan be given in degrees.
Revisitation fixes
- Invalid fixation coordinates, onsets and reference coordinates now cause an error instead of silently changing the scored sequence or reference pool. Fractional lag bounds are rejected; large whole-number bounds work without integer overflow. Empty
eye_tableinputs return typed summary columns. - Euclidean distances use scaling to avoid overflow or underflow from squaring coordinate differences.
nearest_distanceandmatch_distancedistinguish the nearest eligible fixation from the most recent match;distanceremains an alias fornearest_distance. - Fixation results retain
dist_method; summary-onlyeye_tableresults retain all scoring and null-selection arguments in therevisit_specattribute. - Incremental reference coverage preserves the union of eligible neighbourhoods while removing full distance matrices and repeated reference-history scans. Missing expectations, including partial reference support, remain
NA. - Documentation distinguishes spatial returns from backtracking, and describes observed-minus-expected as a descriptive correction. The vignette no longer applies an independent-trial t-test to corrections from shared reference pools.
eyesim 0.3.0
Bug fixes (peyesim parity testing, #42-#50)
-
match_scale()no longer always errors. The row columns frompmap()were passed toestimate_scale()as extra arguments (#42). - The internal
crqa()wrapper returns a 1x1 recurrence plot when embedding leaves a single point, instead of the 2x2 plot that crqa’s embedding produced by dropping a dimension. It errors when no embedded point is left (#43). -
c()on fixation groups skips an empty leading group instead of turning every later onset into-Inf(#44). - The fast cosine path in
template_similarity()scores every pair of all-zero maps as 1, matchingsimilarity(). Before, only the first pair was 1 (#45). -
template_similarity(multiscale_aggregation = "none")returns NA for a row with a NULL map instead of failing inbind_rows()(#46). - NULL or invalid templates are no longer drawn as permutation controls. With
method = "emd"they aborted the call, and other methods counted them inn_perm(#47). - Multiscale permutation baselines are averaged per sigma. They were averaged by position, so controls listing their sigmas in a different order were mixed across scales (#48).
-
cca_transform()with an NAfit_byvalue leaves those rows unfitted with a warning, instead of erroring (#49). - MultiMatch
mm_durationis 1, not NA, when aligned durations are all zero.mm_position_emdweights fixations equally when a scanpath has no total duration, instead of reporting a perfect match (#50).
Bug fixes (pre-CRAN review)
-
coral_transform()applied the CORAL map in the wrong order (X Ct^1/2 Cs^-1/2instead ofX Cs^-1/2 Ct^1/2), so adapted source scores did not take on the reference covariance. Results from CORAL change. -
cca_transform()left raw PCA scores in the columns after the canonical variates, so similarity mixed two coordinate systems; those columns are now zero. Rows of a group with no fitted CCA model keep PCA scores and now raise a warning. Single-component latent models no longer build the wrong matrix square root. -
emdw()(EMD used bysimilarity(method = "emd")and MultiMatch position EMD) now normalises weights for every backend, so emdist, transport and T4transport agree; zero-mass cells are dropped first, which makes EMD on density grids much cheaper. The transport fallback, which errored, works. The saliency-residual EMD keeps raw masses (partial matching) and requires emdist. - The internal CRQA wrapper passed the fixation index instead of
ytocrqa::crqa()and ignoreddelay,embed,rescaleandmetric. -
fixation_overlap()(andsimilarity(method = "overlap")) no longer counts time points before the first fixation in the denominator ofperc. -
density_by()works withoutgroups(defaultNULL). -
calcangle()no longer returnsNaNfor (anti)parallel vectors. -
suggest_sigma()returnsNArather than0when coincident fixations give no spread and no display bounds are supplied. -
template_regression()gainsdensity_varand gives a clear error when a source row has no baseline map. -
fixation_group()andrep_fixations()accept empty groups. -
gaze_replay_cv()andgaze_baseline_cv()lower the defaultn_foldswhen a fold would otherwise hold fewer than two candidate items in a contrast stratum (previously an error), and give a clear error for an explicitn_foldsthat does. Replay’s inner calibration counts distinct items, not rows, when deciding whether inner folds are possible. - Transport: the native backend no longer accepts a log-domain fallback plan under
projection_method = "standard"(such specifications now use the reference backend, as"log"does); reference-backend stationarity is no longerNAwhen the last entropy stage takes no step; native iteration counts no longer reportmaxit + 1; prepared paths built under a different chronology are rejected. - The Python MultiMatch bridge (
multimatch_gaze) now actually imports the module. - Density legend labels fall back to
>=in non-UTF-8 locales instead of erroring.
Performance
-
template_similarity(),fixation_similarity()and the other similarity wrappers no longer run future’s automatic globals scan over the whole source table on every call (about 0.4 s per call, even under the default sequential plan). Results and random streams are unchanged, including under parallelfutureplans. -
gaze_transport_cv()solves each (source row, candidate) alignment once per call under the identity warp instead of once per fold, and prepares each candidate’s reference episodes once (2.4-4x faster; results identical). -
gaze_transport_cv()scores rows in parallel under afutureplan with more than one worker (see?gaze_transport_cv). Results are identical to a sequential run, and alignments solved on workers still feed the cross-fold cache. On 4 cores, a 48-row real-data run took 5.8 min instead of 18.0 min with a fitted warp and 2.4 min instead of 4.2 min with the identity warp. The native Transport solver no longer creates.Random.seed, which it never used. - Replay’s inner calibration fit prepares each row’s loss once instead of on every optimiser step (about 5x faster). Results are identical.
-
sample_density_time()normalises each template and looks up each source row’s grid cells once, then reuses them for the observed value and every permutation (about 25x faster on 200 rows x 100 permutations; results and random streams identical). -
similarity(method = "dcov")computes distance correlation with the O(n log n) univariate algorithm (energy::dcor2d()on standardised maps) instead of the O(n^2) one: a pair of 100 x 100 maps takes 0.04 s instead of about a minute, with the same value to within 1e-12.
Packaging
The
wynn_study_imagedataset is removed to keep the package within CRAN’s size limit. It was unused, was documented as an image although it held study fixations pooled by image version, and can be rebuilt fromwynn_study(seedata-raw/process_dat.R).src/Makevarslinks LAPACK/BLAS (arma::solveneeds them) and no longer forces C++14;SystemRequirements,Remotesand the unusedcolorplanesuggestion are removed; base packages used via::are declared.
Other changes
-
GazeWeave stabilization:
- Fold assignment and the typicality subsample use R’s default generator and locale-independent (radix) ordering whatever
RNGkind()and locale are in effect, so a seed gives the same folds everywhere; the caller’s generator and stream are still restored. With non-default generator settings or locales, folds can differ from earlier versions. ANULLseed passed directly togaze_transport_cv()orgaze_replay_cv()is an error rather than a random fold assignment. - When an outer fold cannot fit its temperature calibration (too few items per contrast stratum), Replay and Transport now raise one warning of class
gaze_calibration_fallbackper fit, record the count incalibration_fallback_folds, and show it inprint(). Previously such fits silently returned 0 bits (declared-prior fallback) or uncalibrated scores. -
gaze_transport_spec()rejects a spatial model whose unit contradicts the screen’s, andgaze_transport_cv()warns (classgaze_spatial_scale) when fixation coordinates are far coarser than the spatial kernel, as with pixel data and the degree-scale default bandwidths. -
gaze_screen(center = )now sets the screen limits around the centre (e.g.c(-15, 15)for a 30-degree screen centred on 0); previously the limits were always[0, width], so centred coordinates were clamped. Replay warns (classgaze_screen_clamping) when more than 5% of fixations lie off the declared screen. - Errors about missing columns name the columns.
-
gaze_transport_cv(),gaze_replay_cv()andgaze_baseline_cv()check every source path (and Replay and baseline reference paths) before fitting. A path with missing or non-finite values, no fixations, negative or zero total duration, or duplicated onsets is reported by table, row number and match keys (up to five rows, then a count), instead of failing mid-fit without saying which row. Transport still drops and records invalid reference episodes per candidate, as before.gaze_baseline_cv()now also names missing columns. -
gaze_transport_cv()gainskeep_alignments = c("true", "all", "none"). By default a fit now keeps only each row’s true-candidate alignment, which is all the alignment plots and diagnostics use: a 48-row, 12-candidate real-data fit shrinks from 78 MB to 6.8 MB (0.3 MB with"none"). Scores, candidate tables and the solver summary are unchanged;"all"keeps every candidate’s alignment as before. - One documented result schema: the new “Result columns” section of
?gaze_weave_cv(inherited by?gaze_transport_cv,?gaze_replay_cvand?gaze_baseline_cv) lists the columns every engine returns, and a test enforces them. To get there,gaze_replay_cv()andgaze_baseline_cv()results gainprior_true(it was Transport-only, thoughgaze_info_bits = log2(posterior_true / prior_true)everywhere), and Transport fits and alignments record their estimator revision asprovenance$revision, like Replay, instead ofprovenance$solver_revision. - Paths prepared for batch Transport scoring are now refused under a spec with a different
coalesce_distance, not only a different neighbour count; such a path had merged different fixations. -
gaze_weave_cv()gainstemplate_on, passed to Replay. -
gaze_local_order(),gaze_transport_prepare(),gaze_transport_align_batch()andelastic_consensus_align()are no longer exported; they were internal plumbing. - Long native Transport solves can be interrupted.
- Transport 2026.10 solves each fixed-mass projection with at most 10 plain Sinkhorn sweeps before handing over to its damped-Newton finisher (both backends), and the Newton and stationarity systems use a Cholesky solve with a conditioning guard. A pair alignment is about 2.6x faster; scores agree with the previous solver to about 1e-8. A looser stopping tolerance was rejected: at
tolerance >= 1e-5some nodes stop at a worse local solution (0.015 nats on real data). - The Replay estimator revision
"2026.08"is retired because of its documented defects: unnormalised screen emissions, a single Student background density, an inflated replay scale, and a duration grid under which the no-replay null was not identifiable.gaze_replay_spec()still takes and recordsrevision, but"2026.10"is the only accepted value;revision = "2026.08"is an error that names the git taggazeweave-2026.08-frozen, from which 2026.08 can be reproduced. Replay specifications, models and fits saved without a revision used to be read silently as"2026.08"; they are now an error asking you to re-create the specification withgaze_replay_spec(). Thegrid_sizeargument ofgaze_replay_spec(), which"2026.10"ignored, is removed, as is thegrid_sizeelement of Replay specifications. The unused transition grid-search fields (transition_candidates,candidate_log_likelihood,selected_candidate) are removed from fitted models’trainingelement. Revision"2026.10"results are unchanged. Transport revision"2026.08"is retired as well (below). - The Transport solver revision
"2026.08"is retired because of its documented defects: a chronology residual that jumped as selected edge mass vanished (0/0 for one-fixation sources), a relative-change stopping rule that could stop after one heavily backtracked step, a reference backend that silently restarted non-converged nodes, and silent native fallbacks.gaze_transport_spec()still takes and recordsrevision, but"2026.10"is the only accepted value;revision = "2026.08"is an error that names the git taggazeweave-2026.08-frozen, from which 2026.08 (and the frozen validation courts that pin it) can be reproduced. Transport specifications saved without a revision, or with the estimand name in therevisionslot, used to be solved silently as"2026.08"; they are now an error asking you to re-create the specification withgaze_transport_spec(). The revision-dependent defaults become plain defaults:entropy_schedule = c(0.15, 0.05, 0.015),tolerance = 1e-6andreliability = "none"(still"effective_fixations"withgaze_calibration_control(method = "global")). The legacy solver paths are removed from both backends, including revision 0 of the native solver. Revision"2026.10"results are unchanged.
- Fold assignment and the typicality subsample use R’s default generator and locale-independent (radix) ordering whatever
GazeWeave (
gaze_weave_cv(),gaze_transport_cv(),gaze_replay_cv(),gaze_baseline_cv()and their specifications) is documented as experimental: its interface and result columns may still change.density_matrix(), an exported generic with no methods, is removed.Examples for
similarity(),fixation_similarity(),sample_density_time(),center(),normalize(),add_scanpath(),coords(),get_density()andsample_fixations()now run, and the datasets document every column.wynn_studypools each participant’s four study viewings of an image into one row;data-raw/process_dat.Rnow reproduces it.Every exported GazeWeave function now has an example, built on a small shared study/recall dataset (
inst/examples/gaze-weave-data.R). Cross-validated fits are in\donttest{}and take a few seconds.print.fixation_group()ignores missing coordinates and handles empty groups.The research validation material in
inst/validationstays in the source repository but is no longer part of the package.Plot titles, subtitles and captions wrap to the width they are drawn at, in lines of even length (new
element_text_wrap(), used bytheme_eyesim()), so long GazeWeave titles no longer run off narrow figures. Where a layout (such as side-by-side patchwork) reserves less height than the wrapped text needs, the text is set smaller to fit rather than overprinting. Newlines in a label are kept as line breaks. A user’stheme(plot.title = element_text(...))merges into it and keeps wrapping. The package’s long GazeWeave labels also carry line breaks, so they stay readable under other themes.plot.fixation_group()numbers and joins fixations in onset order, even when rows are not sorted by onset.Fixations whose markers overlap, and so cannot be numbered individually, are outlined together (a ring for a single fixation) and labelled once (“4, 9, 17”), with a leader that stays clear of every other fixation. Numbers still without a position are listed in the in-panel note (or counted, when more than six).
With a finite upper
limit, colour bars label the limit “≥ limit” (to three significant digits), and density at or above it takes the top colour. The scale is the same whether or not a plot’s data exceed the limit, so plots sharinglimitsshare colours and one collected colour bar.
eyesim 0.2.0
-
All plots share one visual system. New exports:
theme_eyesim(),theme_eyesim_spatial(),eyesim_colours(),scale_fill_eyesim_density()andscale_colour_eyesim_time(). Reference/encoding and source/recall gaze, correspondence, diagnostics, time and density each keep one colour or ramp acrossplot.fixation_group(),plot.eye_density(),anim_scanpath()and the GazeWeave Replay and Transportautoplot()methods. Visible changes:-
bg_imageis drawn with its stored colours. It was contrast-stretched. - Stimulus-space plots keep a 1:1 aspect ratio, and
xlim/ylimno longer drop fixations outside them, so density contours close. - Density is transparent at zero and has a colour bar.
plot.fixation_group()andplot.eye_density()gainlimits(put several maps on one scale) andlegend;plot.eye_density()gainscolours. -
plot.fixation_group(transform = )now transforms the density colour scale, as documented. The fill was always cube-root before, so the default ("identity") now fills linearly and low densities look lighter.coloursis now used; it was ignored.alpha_rangedefaults toc(0, 0.9). Point area is proportional to duration. -
plot.eye_density(alpha = )is the opacity at maximum density; lower densities are more transparent. -
anim_scanpath()titles frames by what it animates over (“Fixation N” or “Onset N”) and keeps earlier fixations as faded marks. -
type = "density"andtype = "filled_contour"draw non-overlapping density bands between evenly spaced edges from zero, coloured on the shared density scale (so they honourtransform,coloursandlimits). The band containing zero is transparent, and a stepped colour bar shows the band edges. Densities abovelimitstake the top colour. Over abg_image, thin white contour lines keep density edges visible. - Fixation numbers are placed when the plot is drawn, at its actual size: next to their own fixation, or further out with a leader line that stays clear of every other fixation and leader. Numbers are never drawn over another number or marker. The first and last fixations are placed first, and a ring marks the first fixation (its leader starts at the ring). Numbers that cannot be placed unambiguously are omitted, and a note inside the panel (unaffected by
labs(caption = )) says how many. Rank colour bars show no tick labels. -
plot.fixation_group()gainsaspect = c("equal", "free"):"free"fills the panel at the cost of true angles and shapes. With sharedlimitsbelow the peak, the stepped colour bar gains a “> limit” step, and a lower limit is ignored (with a warning) for the banded types. - GazeWeave overlays have a legend for the reference/source paths. Braid widths, arrow opacity and point areas are scaled from zero (widths and opacity keep a small visible floor). In both braids, point area is fixation mass and source points are filled by the share of their mass with a correspondence (faint = unmatched, or background in Replay).
- Requires ggplot2 >= 4.0.0 and imports viridisLite.
-
fixation_entropy()on a fixation group acceptsaggregate. With a vectorsigma,aggregate = "none"returns one entropy per scale. It was passed on toks::kde(), which rejected it.sample_fixations(fast = FALSE)now returns the same object as the default path: adata.framewithduration = 1, and all four columns for an emptytime. It returned a tibble withdurationNA before the first onset and 0 after, and a column-less tibble for an emptytime.-
gaze_transport_spec()gainsrevision = c("2026.10", "2026.08"). The default,"2026.10", changes the Transport solver in both the native and the reference backend."2026.08"reproduces the frozen August 2026 solver bit for bit, and the frozen validation courts now pin it. (Transport"2026.08"was retired in eyesim 0.3.0; see above.) Specifications saved before revisions existed are solved as"2026.08"(an error since eyesim 0.3.0); an unknown revision is an error. Under"2026.10":-
Estimand change (unreleased). The chronology residual is
1 - 2A / (R + S + kappa)withkappa = 1e-3, one constant shared by both backends. It was1 - 2A / (R + S), set to 0 whenR + S <= 1e-15. That definition jumped by up to 0.1 in the objective and was 0/0 for one-fixation sources. The new residual is continuous, has a bounded gradient, and tends to 1 as selected edge mass vanishes. The limit 1 is the neutral value: a source without edges gets the same chronology term from every candidate. -
Stopping rule (first-order).
toleranceis now in objective units (nats), with default1e-6. A stage stops when three conditions hold: the plan is feasible, no negligible-mass cell is trapped, and a first-order model predicts at mosttoleranceof remaining decrease. The model sums entropic single-cell relaxation gains on real cells and a quadratic model on slack cells, both from least-squares dual prices. This is a stopping heuristic, not an optimality certificate. On a 60-pair review set, a tight continuation of stopped nodes lowered the objective by at most 9.7e-7 at the 95th percentile. On 9 of 692 nodes it still found 1e-5 to 1.4e-3, along slow directions or away from saddles. Tolerances of 1e-7 to 1e-9 did not remove those nodes and multiplied stalls. -
Traps and steps. A reviewer showed that zeroing a support cell could produce plans that a weighted measure alone certified, 2e-6 to 25 above the node optimum. Such cells, including cells at exactly zero, are now detected from their uncapped relaxed mass and reseeded (a 1e-3 mixture with the independent start). All 40 review traps then re-converged within 4e-7 or stayed uncertified. Of 332 exact-zero variants, none certify in place. After reseeding, 24 settle in a different local optimum 1e-4 to 0.14 above the original. Every mirror step is capped at
min(step_size, 50 / max|g|), so the exponent clamp never binds. -
Line searches, stalls and
maxit. Both revisions accept a step that raises the objective by up to 1e-12 relative. A line search therefore fails only when every trial rose by more than that, which at 1e-8 projection accuracy is noise-dominated. The native backend reported this as a numerical failure, andbackend = "auto"then silently re-solved the pair with the 30-200x slower reference backend. A noise-limited line search now ends the stage as"stalled_projection_limited". Reachingmaxitends the node as"not_converged", including underbackend = "optimized"; it previously raised an error on 14 of 60 review pairs atstep_size = 0.05. Both statuses are scored, recorded, and counted bygaze_transport_cv(). - Projection. When standard Sinkhorn exhausts its iterations, a damped log-domain dual Newton projection takes over, then log-domain Sinkhorn, in both backends.
-
Starts and entropy schedule. Every coverage node is solved from the independent start and the adjacent-coverage continuation, replacing the reference backend’s silent cold restart.
multistart = 2adds a spatial start and is now native. The default entropy schedule isc(0.15, 0.05, 0.015). -
Known residual error: local optima. The objective is non-convex, and the default score can fall short of a multistart oracle. The error is one-sided (default scores are lower than the oracle’s) and depends on the pair mix:
- On our 60-pair review set, the gap was at most 0.12 nats (0.39 null-score SD), with 95th percentile 0.0056.
- On a second reviewer’s 88-pair set (half null), the gap was at most 0.117 nats (0.24 SD), with 95th percentile 0.023 (0.047 SD). The 95th percentile was 0.033 for null pairs and 0.006 for matched pairs. The error is concentrated in null and sparse-source pairs.
multistart = 2never moved a score by more than 2e-5 on the reviewer’s set; on ours it lowered the 95th percentile only to 0.0053 at 1.48 times the runtime. It is therefore opt-in. Scores also still depend onstep_sizeat this level.
- The mutual-information term is evaluated in the log domain when the product of two marginals underflows.
-
Backends and fallbacks. Under
backend = "auto", specifications the native backend cannot solve (projection_method = "log") go to the reference backend for every pair. Any per-pair numerical fallback raises a warning of classgaze_transport_backend_fallback. Every alignment records its backend, fallback, status and solver revision inconvergence.gaze_transport_cv()reports fallback, stall and not-converged counts per row and insolver. Validation checkpoints record and verify the solver revision. -
Polish gap. The polish Frank-Wolfe gap is
NA, with a reason, where a selected mass with a positive target vanishes. The Jensen-Shannon gradient is unbounded there, so the reported gap came from the gradient floor. The objective is non-convex, so the gap measures stationarity only.
-
Estimand change (unreleased). The chronology residual is
-
GazeWeave Replay gains
gaze_replay_spec(revision = c("2026.10", "2026.08")). (Replay"2026.08"was retired in eyesim 0.3.0; see above.) The new default,"2026.10", changes Replay as follows:- The observation unit is one recall fixation. The HMM makes exactly one hidden-state visit per recall fixation (background, or one state per encoding fixation, with the same transition structure as before), and each fixation emits its position and its duration. Durations are log-normal: replay states have log-duration mean
a + b * log(encoding duration), and the background has its own log-normal. Both are normalised densities whose parameters are shared by every candidate. The candidate score is the total trial log likelihood, not a mean per duration bin, so evidence grows with the number of recall fixations.grid_sizeis ignored under"2026.10"(a one-time message says so when it is supplied). - When a
screenis declared, each replay emission is a Student density truncated to the screen and normalised by its on-screen mass. Under"2026.08"the on-screen mass was 0.99 at the centre and 0.35 near a corner. Encoding fixations recorded off the screen are projected onto it, both in the emissions and in the EM scale update. Without a screen, the support is the whole plane, and a one-time message says so. - The background state’s position density is a screen-truncated kernel density of recall fixations (equal weight per fixation within a trial), mixed with a uniform screen floor whose weight EM fits.
"2026.08"used a single Student density at the training recall mean. The evaluation protocol is declared bybackground_support:- Under
"training"(the default), backgrounds come from training recalls only. Thebackground_bylevel is used when it has at least three trials, otherwise the population density. - Under
"held_out"(opt-in), a held-out participant’s own recalls of items outside the scored row’s candidate pool are used. It engages only with an item-level split and acontrast_onthat blocks items; it never engages under participant holdout with shared items. It is transductive: it reads other evaluation rows’ item labels, so it cannot be used on unlabelled recalls. - At scoring time the background drops training rows whose exclusion key (
setdiff(match_on, background_by)) matches any candidate in the pool, never only the true one, so it is independent of the label. Whenbackground_byis not inmatch_on, that key includes the participant, so other participants’ recalls of the candidate items remain. If the exclusion would leave fewer than three trials, nothing is excluded. An earlier draft excluded only the true item, which leaked the label: on an ambiguous null, top-1 accuracy with two candidates rose from 0.48 to 0.90. - Pooled fallbacks are reported by a message and counted in
gaze_replay_cv()provenance (background_fallback), as are background levels unseen in training (e.g. under participant holdout).
- Under
- The replay scale, the duration parameters, and all four transition probabilities are fitted by Baum–Welch EM on training rows only.
transition_gridnow supplies only the starting values. The background entry, initial and persistence probabilities may all approach one, so a fully null recall can be represented. The replay scale is capped at one sixth of the shorter screen side: a replay state is local, and a near-uniform replay state cannot be distinguished from the background. EM reports non-convergence if the likelihood ever decreases. - On data simulated from the fixation-level model, EM recovers the replay scale to within 1%, the background share to within 0.02, and the duration slope
bto within 0.01."2026.08"inflated the scale by 60%. With the validation-court transition grid, it also could not exceed a stationary background share of 0.67 under a full null. - An earlier, unreleased draft of
"2026.10"kept the duration-grid observation model. It treated repeated bins of one fixation as independent emissions, so replay and background were not identified: on a uniform null with 20-30 fixations at 64 bins the background share was 0.44-0.62. The fixation-level model fits the same null with a background share of 0.98, and the candidate score spread is about 1% of a typical true-candidate margin. The draft’s repeated-grid message andprovenance$repeated_grid_rowsare removed, because"2026.10"is unreleased; its results are not comparable with this version. - Fitted models record
revision, EM diagnostics (training$em), and the background model. Theirversionis 5. -
gaze_replay_align()andgaze_replay_align_episode()gainbackground_key. Revision"2026.08"reproduces the earlier fits and scores bit for bit. The frozen validation court scripts pin it. - Specs and models saved without a revision are treated as
"2026.08"(an error since eyesim 0.3.0). An unknown revision is an error. Multi-presentation court checkpoints record the Replay revision.
- The observation unit is one recall fixation. The HMM makes exactly one hidden-state visit per recall fixation (background, or one state per encoding fixation, with the same transition structure as before), and each fixation emits its position and its duration. Durations are log-normal: replay states have log-duration mean
-
Shared candidate calibration for Transport and Replay under revision
"2026.10"(unreleased), configured by the newgaze_calibration_control()and thecalibration_controlargument ofgaze_transport_spec()andgaze_replay_spec(). Revision"2026.08"(since retired for both engines) is unchanged: its specifications carry no control, and its cross-fitted results areidentical()to those before this change (pinned bytests/testthat/fixtures/calibration_base_0d9fbcf.rds, fitted at master 0d9fbcf). Replay fitted withoutcontrast_onstill scores at temperature one, as before; with too few inner candidates it now returns the declared prior instead of temperature one.-
Evidence-scaled temperature. Each held-out row is scored at
T_i = T * (n_ref / n_i)^gamma, wheren_icounts the row’s evidence (Transport: duration-effective recall fixations; Replay: recall fixations) andn_refis their geometric mean over the calibration rows.Tandgammain[0, 1]are fitted jointly on inner out-of-fold rows only.gamma = 0is one global temperature. The engine’s lower temperature bound caps every row’s confidence. Results gaintemperature(per row),inverse_temperatureandevidence_count;fit$calibration$fold_calibrationrecords each fold’s fit. -
Prior. The prior on
log Tcentred atT = 1is replaced by a zero-centred Gaussian prior (sd 3) on the standardised inverse temperaturesigma / T, wheresigmais the median within-row standard deviation of the inner scores. It pulls only toward the declared candidate prior. The old prior pulled Replay’s total trial log likelihood, whose natural scale is several nats, toward overconfidence. -
Stein shrinkage. The fitted inverse temperature is multiplied by
max(0, 1 - p / LR), withLRthe inner likelihood-ratio statistic against the declared prior andpthe number of free parameters. With a few dozen inner rows a null fit otherwise returns a positive inverse temperature about half the time. -
Ranking is calibration-independent. Calibration changes probabilities and bits only. Under
"2026.10",template_rankandtop1_credit(candidate and per-episode) come from one fixed, label-free reference ranking for both engines and both methods:log prior_k + r_k, withr_kthe native candidate score after any typicality offset (Transport: log mean of the episode scores at inverse temperature one; Replay: trial log likelihood), whatever the fittedT,gammaor Stein factor. Previously they followed the calibrated posterior. That is not calibration-invariant for multi-episode Transport, whose inverse temperature acts inside the log mean over episodes: episodes a = (0, -10) and b = (-3, -3) rank b first as1/T -> 0(arithmetic mean) but a first atT = 1. The earlier rule for a calibration that returns the prior (prior first, then the ranking score) was therefore not the1/T -> 0limit, and the claim that calibration never changes the ranking was false for multi-episode Transport. A test pins identical top-1 and rank AUC across"evidence_scaled","global", an unshrunk fit and a Stein factor of zero on a two-episode Transport CV. AUC should be computed from ranks orranking_score, not from the calibrated probabilities. -
Kappa. The effective-fixation reliability shrink is dropped from the default: it could only temper sparse rows, and with
gammait was a second, weakly identified parameter on the same axis. Transport’sreliabilitynow defaults to"none"under"2026.10", and"effective_fixations"requiresgaze_calibration_control(method = "global"). That method reproduces the pre-change"2026.10"probabilities and bits exactly only withreliability = "effective_fixations"passed explicitly for Transport (the new default"none"fits a different model); a saved"2026.10"specification without a control is treated the same way. Its rank and top-1 follow the reference ranking above and so differ from the pre-change values only where the fitted temperature or kappa mixture had reordered candidates (multi-episode Transport or a non-uniform prior); the pinned fixture, single-episode with a uniform prior, isidentical(). -
Typicality offset (default
"standardized"for Transport,"none"for Replay).typicality = "mean"subtracts each candidate’s mean score against a seeded, item-balanced subsample of the fold’s training recalls of other items (inner rows use their inner training rows). Scores are centred within each source and the means are shrunk by an empirical-Bayes factor."standardized"also divides by each candidate’s shrunk score standard deviation over the sources. The offsets never read a held-out recall or label, so relabelling a held-out row leaves every candidate score unchanged. Adjusted scores are ranking scores, not normalised likelihoods. Mean subtraction does not equalise score variances;fit$calibrationreports per-candidate dispersion before and after. - Simulated evidence (all synthetic; numbers from
inst/validation/gaze-weave-calibration-a3.R, gates asserted by the slow checks intests/testthat/test_gaze_weave_calibration_revision.RwithEYESIM_SLOW_TESTS=true). “Before” is the pre-change"2026.10"calibration.- Centre-biased nulls, pooled over four seeds: Replay mean bits -0.107 (SE 0.034) before, 0.000 after; Transport -0.015 before, 0.000 after. Top-1 stays within MC error of chance. Every fold’s Stein factor was zero, so the declared prior was returned. With the new prior but without the Stein factor, Replay stayed at -0.10.
- Transport with 2- and 8-fixation recalls: held-out log loss 0.885 before, 0.939 with
gamma = 0, 0.760 with fittedgamma(0.72 to 1 across folds). Mean max-p minus accuracy moved from +0.13 / -0.11 (sparse / rich) to -0.03 / -0.02 on one seed and from +0.04 / -0.27 to -0.06 / -0.05 on the other. Top-1 and rank-based AUC are unchanged. - Replay with 6- and 30-fixation recalls: fitted
gammais 0 to 0.3, because the total log likelihood already grows with the fixation count. Held-out log loss 0.626 before, 0.631 withgamma = 0, 0.653 fitted: no calibration gain for Replay on this fixture. - Typicality, centre-biased null, six candidates per pool (two central encodings), argmax share per central candidate (chance 0.167): Transport 0.396 without an offset, 0.128 with
"mean", 0.160 with"standardized"(MC error 0.020); Replay 0.296, 0.271 and 0.266 (MC error 0.015). The offsets equalise the candidates’ held-out means (Transport between-candidate SD 0.32 to 0.08) but not their variances: the per-candidate SD ratio was 9.3 before and 6.0 after"standardized"(11.1 after"mean") for Transport. For Replay it exceeded 1900 before and after, but this is not a heavy-tail effect: under these nulls the HMM background absorbs most recall fixations, and in 57% of rows every candidate lay within 0.01 nats of the others. The SD ratio comes from these near-degenerate pools, and Replay’s residual central bias from the argmax among near-ties (central candidates won 0.54 of the tie rows against a 0.37 share of candidates; few rows). Replay top-1 and AUC should therefore be computed fromranking_scorewith a tie tolerance declared in advance (for example 0.01 nats), splitting credit among tied candidates. - Discrimination on signal data: Transport top-1 0.781 / AUC 0.942 without an offset, 0.781 / 0.944 with
"standardized", 0.771 / 0.940 with"mean"; Replay 0.938 / 0.985 without, 0.951 / 0.988 with either offset.
-
Evidence-scaled temperature. Each held-out row is scored at
Canonicalized edge-normalized Transport as the sole public Transport method. The API is now
gaze_transport_spec(),gaze_transport_align(),gaze_transport_cv(), and related unversioned helpers;gaze_weave_cv()acceptsengine = "transport". The unreleased original and v2 implementations, exports, S3 classes, tests, and documentation were removed. Their labels remain only in internal and immutable development-court provenance.Added edge-normalized GazeWeave Transport as the symmetric explanatory alignment engine. It integrates bounded coverage, scores separate study presentations as fixed equal-prior likelihoods, uses nested response-blind calibration, and retains a pure-R oracle alongside the optimized native backend. Default tidy output contains only identifiers and
gaze_info_bits; coverage, coordinate-unit spatial RMSE, local order, contraction, rank, episode weighting, and solver stability are nested diagnostics.Added Transport registered overlays, optimized-correspondence gaze braids, raw-versus-registered paths, and a one-score evidence ledger. Labels state that Transport correspondence is an optimized alignment, not posterior replay probability.
Froze and ran the Transport simulation, full-cohort repeated-viewing, and exact 0–3000 ms retrieval courts. Invariants, runtime, and the study positive control passed. Retrieval information and held-out old-item response prediction did not pass their uncertainty gates, so Transport is retained as an explanatory engine without a retrieval or superiority claim.
Added GazeWeave Replay, a directional probability model of locally ordered, restartable encoding-to-recall replay mixed with candidate-independent background gaze. Candidate probability is calibrated on inner-held-out items within each outer-training set from mean log predictive density per normalized duration bin, with log-domain evidence and a weak fixed residual-temperature prior. Raw total HMM likelihood remains available as a diagnostic. The primary endpoint is
gaze_info_bits = log2(p_true / prior_true).Added engine-specific registered overlays, gaze braids, and diagnostics. Replay braids show posterior correspondence probabilities. Transport braids are labelled as regularized optimized correspondences, not posterior uncertainty.
Added frozen synthetic and real-data validation courts under
inst/validation. The real court compared Replay and the then-current Transport development estimator with nested-ridge MultiMatch, registered density, and elastic-matching baselines under shared participant-by-item folds. The synthetic court passed all gates. The real court failed its repeated-viewing gate after calibration exposed unstable Replay probabilities, sogaze_weave_cv()requires an explicit engine choice and no GazeWeave superiority is claimed. Registered density was strongest overall and blank-screen imagery evidence was weak for every method.Added a prospectively frozen participant- and item-disjoint replication of the resolution-invariant Replay calibration. The correction restored positive held-out Replay information in repeated viewing, but neither GazeWeave engine had positive mean information in the blank-screen control. The package therefore retains explicit engine selection and makes no power or superiority claim.
template_similarity(),fixation_similarity(), andscanpath_similarity()now return ann_permcolumn whenpermutations > 0. It records the number of permuted non-matching comparisons that actually contributed toperm_simfor each row, which varies with smallpermute_onstrata or when fewer candidates than requested are available, and is0when no baseline could be computed (perm_sim = NA). Use it to exclude rows with too thin a permutation baseline, e.g.dplyr::filter(res, n_perm >= k). The column is additive: output forpermutations = 0is unchanged.
Correctness fixes from the eyes4s parity audit
eye_density()now honours an explicitweightsvector. It was previously ignored, so the map was always unweighted unlessduration_weighted = TRUE. Weights must be finite, non-negative, not all zero, and supplied one per fixation; they are subset along withwindow, and they take precedence overduration_weighted. If the weights of the fixations left afterwindowsum to zero, orduration_weighted = TRUEand every duration is zero,eye_density()now warns and returnsNULLunder both backends. The ks path previously failed with “missing value where TRUE/FALSE needed”, or, withnormalize = FALSE, returned a map ofNaN.eye_density()now forwards extra named arguments in...toks::kde(), as documented. Previously any extra argument failed with “unused argument”. Arguments thateye_density()sets itself (includingeval.points), and unknown names, are rejected with a clear error, as are extra arguments underkde_pkg = "MASS".eye_density()now requireskde_pkgto be"ks"or"MASS". Any other value, such as a mistyped"ks ", previously fell back to MASS silently.Weighted densities under
kde_pkg = "MASS"(explicitweightsorduration_weighted = TRUE) now work. The internal weighted kernel failed with “invalid ‘times’ argument” andeye_density()returnedNULL.eye_density()and the geometric warp behindaffine_transform()andcontract_transform()now round maps with a fixedzapsmall(digits = 7). They previously usedgetOption("digits"), so the same call returned maps differing by up to about 5e-9 when a session changedoptions(digits). Results under the defaultdigits = 7are unchanged. The documented rounding rule was checked against R 4.5.1.The permutation baseline in
template_similarity(),template_similarity_cv(),fixation_similarity(), andscanpath_similarity()now removes the true match before sampling candidates. It previously sampled first and then dropped the match, so with three candidates andpermutations = 2most rows received one control instead of the documented two.n_permis nowmin(permutations, available non-matching candidates). Wheneverpermutationsis smaller than the candidate pool, the sampled controls, and henceperm_simandeye_sim_diff, differ from earlier versions for the same seed.When several source rows share a
match_onkey, every copy of that key is now removed from each such row’s permutation candidates. Previously one copy remained, so a row could be compared with its own template in the baseline. This matchessample_density_time().Permutation candidates are now distinct templates. A template matched by several source rows previously appeared once per matching row, so it could be drawn twice and was counted twice in
perm_simandn_perm. This applies totemplate_similarity(),template_similarity_cv(),fixation_similarity(),scanpath_similarity(), andsample_density_time(), and changes their permutation columns whenever keys repeat within a stratum. Reference rows that no source row matches remain outside the candidate set, as before; the documentation now says so.Documentation:
?template_similarityno longer claims that permutation sampling uses a “fixed future seed”. Sampling uses the session RNG, soset.seed()before the call makes the baseline reproducible. No behaviour change.template_similarity_cv()no longer resets the caller’s random number stream. Fold assignment and permutation draws still run underseed, so its results are unchanged, but the session RNG state is restored on exit. The documentation now states that permutation controls come only from the held-out fold.gaze_weave_cv(),gaze_transport_cv(),gaze_replay_cv(), andgaze_baseline_cv()likewise no longer reset the caller’s random number stream, and no longer create.Random.seedin a session that had none. Fold assignment still runs underseed, so folds and results are unchanged.similarity(method = "fisherz")now returns exactlyatanh(1 - .Machine$double.eps)(about 18.37) for every pair of identical maps, and for any correlation within64 * .Machine$double.epsof 1, such as a rescaled copy of a map; correlations that close to -1 give its negative. Identical constant maps previously returned 1, and identical non-constant maps returned values between about 17.3 and 18.37, depending on rounding error incor(). The clamp is documented in?template_similarity.similarity()on two density maps now refuses maps whose lattices (x and y grid coordinates) differ, instead of comparing thezmatrices cell by cell as if they were aligned. A numericymust have one value per grid cell.template_similarity()and related wrappers raise the same error on both the fast cosine and the general path, and for multiscale maps, where the per-scale error handling previously would have turned it intoNA.repetitive_similarity()also raises it rather than returningNaNwith a warning per pair. Becauseeye_density()defaultsxboundsandyboundsto each group’s own data range, maps built with default bounds generally lie on different lattices and are now refused; pass commonxbounds,ybounds, andoutdimto every map that will be compared.similarity()for fixation groups withmethod = "overlap", and thereforefixation_similarity(method = "overlap"), now defaults todthresh = 60, the value documented forfixation_overlap(). It previously defaulted to 40, so the same pair scored differently through the two entry points.fixation_overlap()now builds its defaulttime_samplesgrid from the onsets of both fixation groups,seq(0, max(c(x$onset, y$onset)), by = 20). It previously usedxonly, so swapping the arguments could change the result substantially (0.098 versus 0.833 in one case).sample_fixations()with the defaultfast = TRUEnow holds the last fixation for time points after its onset, asfast = FALSEalways did. It previously returnedNAthere. It also no longer fails for a group with a single fixation. Downstream,sample_density()withtimes,sample_density_time(),template_sample()withtime, andfixation_overlap()now score time points after the last onset instead of treating them as missing. Two smaller changes on the fast path: fixations with tied onsets previously had their coordinates averaged (approx(ties = mean)) and now yield the last tied fixation, asfast = FALSEdoes; and a fixation withNAcoordinates was previously skipped, carrying the preceding fixation forward, and now yieldsNA. Thefast = FALSEpath now orders fixations by onset first, as the fast path does; for groups whose onsets were not in increasing order it previously returned coordinates by row position rather than by onset.rep_fixations()now countsfloor(duration * resolution)replicates with a floating-point tolerance. A duration of 0.29 at resolution 100 previously gave 28 copies because0.29 / 0.01evaluates to 28.999999999999996; it now gives 29.sample_density_time()now treats everytime_binsinterval as half-open,[lower, upper), including the last, as documented. The final bin was previously closed, so a time point equal to the last break (for example t = 3000 with the defaulttimesand bins ending at 3000) was averaged into the last bin. It now falls outside all bins, which changes the lastbin_*,perm_bin_*, anddiff_bin_*values whenever a sample lies on the final break.multi_match()now computesmm_position_emdover all fixations. It previously used the saccade table, which omits the final fixation, so two three-fixation paths differing only in their last fixation scored 1. Everymm_position_emdvalue changes, including the_permand_diffcolumns fromscanpath_similarity().template_multireg()now defaults tomethod = "lm", as documented. Omittingmethodpreviously failed with “the condition has length > 1”. Unknown methods are rejected bymatch.arg().template_regression()now stops with a clear message when abaseline_keyvalue used bysource_tabappears in more than one row ofbaseline_tab. It previously failed with “$ operator is invalid for atomic vectors”.template_sample()now drops rows whose template or fixation group isNULL, with a warning, as its code comment intended. Theis.null()filter on list columns was a no-op, so a singleNULLtemplate made the whole call fail.fixation_entropy()on a fixation group with a single fixation now returnsNAformethod = "density"whensigmais not supplied, becausesuggest_sigma()cannot estimate a bandwidth from one point. It previously failed with “missing values present in assertion”.eye_density()now reports anNAor non-finitesigmawith a clear message.fixation_entropy()now rejects maps with negative values, such as the difference of two densities, with a clear error. Previously an exact difference map (total zero) gaveNAwhile a signed map with a positive total gave a meaningless number. Maps with zero total mass still giveNA.-
Documentation only, no behaviour change:
-
?eye_density.fixation_groupnow states thatsigmais the kernel standard deviation underkde_pkg = "ks"but theMASS::kde2d()bandwidth under"MASS", whose kernel standard deviation issigma / 4. -
?sample_densitynow states that points are looked up at the nearest lattice point withround(), so exact midpoints resolve half to even on the index scale, and that off-lattice points are clamped to the edge. -
?fixation_entropy.defaultnow states that the grid method counts fixations outsidexbounds/yboundsin the nearest edge cell.
-