Skip to content

Worked example: nonlinear gaze dynamics

This example uses deterministic synthetic data so the workflow can be run in CI. The goal is to demonstrate the analysis contract, not to claim that the synthetic signal reproduces human gaze.

Create a continuous trajectory

import numpy as np
from eyetrajectoriespy import TrajectorySet

time = np.arange(600, dtype=float) * 0.01
x = np.empty(time.size)
x[0] = 0.217

for i in range(time.size - 1):
    x[i + 1] = 4.0 * x[i] * (1.0 - x[i])

gaze = TrajectorySet(
    time=time,
    values=x[None, :, None],
    curve_ids=("demo",),
    dimension_names=("x",),
    time_unit="s",
    coordinate_system="arbitrary",
)

Inspect delay and dimension diagnostics

from eyetrajectoriespy import (
    embedding_delay_diagnostics,
    embedding_dimension_diagnostics,
)

delay_diag = embedding_delay_diagnostics(
    gaze,
    curve=0,
    dimension="x",
    max_lag=30,
    bins=12,
)

dimension_diag = embedding_dimension_diagnostics(
    gaze,
    curve=0,
    dimension="x",
    delay=1,
    max_dimension=5,
    theiler_window=10,
)

These are diagnostics only. The analysis still declares its own \(m\) and \(\tau\).

Reconstruct state space

from eyetrajectoriespy import delay_embed_trajectory

embedded = delay_embed_trajectory(
    gaze,
    embedding_dimension=2,
    delay=1,
    dimensions=("x",),
)

Sparse recurrence and RQA

from eyetrajectoriespy import recurrence_matrix, rqa_metrics

recurrence = recurrence_matrix(
    embedded,
    curve=0,
    target_recurrence_rate=0.05,
    theiler_window=10,
)

metrics = rqa_metrics(
    recurrence,
    min_diagonal_length=2,
    min_vertical_length=2,
)

print(metrics)

The solved radius and achieved recurrence rate remain in recurrence.

Time-varying recurrence

from eyetrajectoriespy import windowed_rqa

dynamic = windowed_rqa(
    gaze,
    curve=0,
    window=150,
    step=75,
    radius=0.05,
    theiler_window=10,
    dimensions=("x",),
)

print(dynamic.table)
print("tail samples not in a full window:", dynamic.dropped_tail_samples)

Local divergence and LLE

from eyetrajectoriespy import (
    local_divergence_curve,
    estimate_largest_lyapunov_rosenstein,
)

divergence = local_divergence_curve(
    embedded,
    curve=0,
    theiler_window=10,
    max_horizon=8,
)

lle = estimate_largest_lyapunov_rosenstein(
    divergence,
    fit_start=1,
    fit_end=5,
)

print(lle.exponent, lle.exponent_unit)
print(lle.r_squared)

The fit interval is deliberately explicit.

IAAFT surrogate test

from eyetrajectoriespy import surrogate_nonlinearity_test

surrogate = surrogate_nonlinearity_test(
    gaze,
    curve=0,
    dimension="x",
    statistic="largest_lyapunov",
    embedding_dimension=2,
    delay=1,
    theiler_window=10,
    max_horizon=8,
    fit_start=1,
    fit_end=5,
    n_surrogates=19,
    max_iterations=100,
    tolerance=1e-6,
    random_state=42,
)

print(surrogate.p_value)

For research use, choose the surrogate count before examining the result and use substantially more than this CI-sized example.

Plots

from eyetrajectoriespy import (
    plot_embedding_delay_diagnostics,
    plot_embedding_dimension_diagnostics,
    plot_local_divergence,
    plot_recurrence,
    plot_surrogate_nonlinearity,
    plot_windowed_rqa,
)

plot_embedding_delay_diagnostics(delay_diag)
plot_embedding_dimension_diagnostics(dimension_diag)
plot_recurrence(recurrence)
plot_windowed_rqa(dynamic)
plot_local_divergence(lle)
plot_surrogate_nonlinearity(surrogate)

See the visual gallery for CI-generated SVG versions.