---
title: "Quantifying path shape"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Quantifying path shape}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment  = "#>",
  fig.width  = 6,
  fig.height = 5
)
```

```{r load}
if (requireNamespace("pkgload", quietly = TRUE)) {
  pkgload::load_all("..", export_all = FALSE, helpers = FALSE, quiet = TRUE)
} else if (requireNamespace("radiatR", quietly = TRUE)) {
  library(radiatR)
} else {
  stop("Package 'radiatR' not installed and 'pkgload' not available.")
}
```

## Overview

Alongside the circular *heading* statistics (see
`vignette("circular-statistics")`), radiatR provides **spatial** metrics that
describe the geometry of each trajectory: how far it travelled, how straight it
was, and how tortuous. These are per-track scalars, returned as tidy data frames
keyed by the trajectory id, computed on the recorded `x`/`y` coordinates.

There are two complementary notions of tortuosity:

- **Displacement-based** --- `straightness_index()` / `tortuosity_ratio()` ---
  compare the net start-to-end displacement with the total distance walked. They
  are simple and scale-invariant, but rely on net displacement, which is
  unreliable for convoluted or random-search paths that end up near where they
  started.
- **Turning-based** --- `sinuosity()` (Benhamou 2004) --- is built from the
  turning angles and step lengths, so it stays meaningful when net displacement
  is small.

The per-observation **kinematics** (instantaneous speed, velocity, turning rate,
`plot_profile()`) live in the main vignette --- see the kinematics section of
`vignette("radiatR")`.

We use the bundled millipede dataset throughout.

```{r data}
data(cpunctatus)
cpunctatus
```

## Path length

`track_length()` is the total distance walked along each trajectory. With the
default unit-circle coordinates it is in unit-circle radii; attach a calibration with
`set_distance_scale()` (or `calibrate_distance()`) and it is reported in physical
units.

```{r length}
head(track_length(cpunctatus))                         # unit-circle radii
head(track_length(set_distance_scale(cpunctatus, 50, unit = "mm")))   # mm
```

The single-path helpers `path_*` operate on plain `x`/`y` vectors if you want a
metric for one trajectory outside a `Tracks`.

## Restricting to within the circumference

Tracks can wander past the circumference (`rho > 1`) -- the animal reaching the
edge, or an occasional tracking outlier. `restrict_to_circumference()` returns a
`Tracks` with that out-of-circumference data removed, so the metrics see only the
within-circle path. `mode = "truncate"` (the default) keeps each track up to its
first excursion beyond the circumference; `mode = "drop"` removes individual
out-of-circumference points.

```{r restrict}
within <- restrict_to_circumference(cpunctatus, mode = "truncate")
head(track_length(within))
```

This is the data counterpart to the plot-only `radiate(clip_tracks = TRUE)`.

## Straightness and tortuosity

The straightness index is net displacement / total length, in `[0, 1]`
(1 = perfectly straight). The tortuosity ratio is its reciprocal, in
`[1, Inf)`.

```{r straightness}
si <- straightness_index(cpunctatus)
tr <- tortuosity_ratio(cpunctatus)
head(si)
# single-path versions on bare coordinates
path_straightness(x = c(0, 1, 2), y = c(0, 0, 0))      # straight -> 1
path_tortuosity(x = c(0, 0, 1), y = c(0, 1, 1))        # L-shaped  -> sqrt(2)
```

## Sinuosity

`sinuosity()` implements the Benhamou (2004) index
`S = 2 / sqrt(p (1 + c) / (1 - c) + b^2)` (`p` mean step length, `c` mean cosine
of turning angles, `b` the CV of step length). A straight path gives `0`; more
winding paths give larger values. Unlike the straightness index it is **not**
scale-invariant --- it has units of `1 / sqrt(length)`, so for the most reliable
comparison across trajectories the path should be sampled at a common step
length.

```{r sinuosity}
sn <- sinuosity(cpunctatus)
head(sn)
```

The three tortuosity measures can be read side by side. Here we join them per
track and average within each stimulus condition (`arc` = target half-width in
degrees, `0` = featureless control):

```{r compare}
metrics <- Reduce(
  function(a, b) merge(a, b, by = "trial_id"),
  list(straightness_index(cpunctatus), tortuosity_ratio(cpunctatus),
       sinuosity(cpunctatus))
)
arc <- unique(as.data.frame(cpunctatus)[, c("trial_id", "arc")])
metrics <- merge(metrics, arc, by = "trial_id")

aggregate(cbind(straightness, tortuosity, sinuosity) ~ arc,
          data = metrics, FUN = function(v) round(mean(v, na.rm = TRUE), 2))
```

## Goal-directed paths and zone occupancy

A second family of spatial metrics summarises *where* a track went relative to a
**target direction** --- the water-maze idiom (probe-trial quadrant dwell, and
crossings of a goal zone). On `cpunctatus` the natural target is the stimulus;
in the landmark-**relative** frame the stimulus sits at angle `0`, so we pass
`coords = "relative"` and `target_angle = 0`.

`count_goal_entries()` counts how many times each track enters a small goal zone
at the target (a `FALSE -> TRUE` crossing of `distance < crossing_radius`):

```{r goal-entries}
head(count_goal_entries(cpunctatus, target_angle = 0, coords = "relative"))
```

`zone_dwell()` gives the proportion of frames each track spends in every
quadrant--ring zone (`Q1` is the target quadrant, the outer ring is the rim):

```{r zone-dwell}
dwell <- zone_dwell(cpunctatus, target_angle = 0, coords = "relative")
head(dwell)
# mean occupancy of the target quadrant (Q1) across tracks
mean(tapply(dwell$proportion[dwell$quadrant == 1],
            dwell$id[dwell$quadrant == 1], sum), na.rm = TRUE)
```

Tune the goal zone with `target_radius` / `crossing_radius` and the annuli with
`ring_breaks` --- see `?count_goal_entries` and `?zone_dwell`.

## See also

The Shiny companion app surfaces these per-track metrics interactively (a
caption of mean straightness / sinuosity and a downloadable metrics table) ---
launch it with `launch_app()`. The per-observation kinematics
(`instantaneous_speed()`, `velocity_vector()`, `angular_velocity()`,
`plot_profile()`) are covered in `vignette("radiatR")`.
