Skip to content

[Metrics] Add function for computing sinuosity (Benhamou 2004) #904

@vybhav72954

Description

@vybhav72954

Is your feature request related to a problem? Please describe.
Sinuosity is a measure of the tortuosity of a random search path, combining the distribution of turning angles with step length statistics. It quantifies how much a path deviates from a straight line during a random search, producing a single scalar where higher values indicate more tortuous movement. The corrected formulation (Benhamou 2004) handles variable step lengths, making it directly applicable to irregularly sampled tracking data without requiring rediscretisation.

It is one of the core trajectory complexity metrics identified in the trajr paper

Describe the solution you'd like

Definition

The corrected sinuosity index (Benhamou 2004, Eq. 8) is defined as:

$$S = 2 \left[ \bar{p} \left( \frac{1 + \bar{c}}{1 - \bar{c}} + b^2 \right) \right]^{-1/2}$$

where:

  • $\bar{p}$ = mean step length
  • $\bar{c} = \frac{1}{n}\sum_{i=1}^{n} \cos(\theta_i)$ = mean cosine of turning angles
  • $b = \mathrm{SD}(p_i) , / , \bar{p}$ = coefficient of variation of step length

Range: $[0, \infty)$, where higher values indicate more tortuous paths. A perfectly straight path has $S = 0$ (since $\bar{c} = 1$). A Brownian-like path has $S = 2 / \bar{p}$ (since $\bar{c} \to 0$).

Alternative names

This metric is also known as: tortuosity index (in some disciplines), corrected sinuosity, Benhamou sinuosity. The original (uncorrected) version by Bovet & Benhamou (1988) uses a simpler formula $S = 2 / [p(1 - \bar{c})]$ but requires constant step length and assumes a wrapped-normal turning angle distribution. The corrected version is strictly more general and should be preferred.

Pseudocode

function compute_sinuosity(data, nan_policy="ffill"):
    # 1. Compute turning angles (depends on movement#833)
    theta = compute_turning_angle(data)
 
    # 2. Compute step lengths
    displacements = compute_forward_displacement(data)
    step_lengths = compute_norm(displacements)
 
    # 3. Handle NaN values per nan_policy
    #    (follow compute_path_length convention)
 
    # 4. Compute summary statistics
    c_bar = nanmean(cos(theta))      # mean cosine of turning angles
    p_bar = nanmean(step_lengths)    # mean step length
    b = nanstd(step_lengths) / p_bar # CV of step length
 
    # 5. Compute sinuosity (Benhamou 2004, Eq. 8)
    S = 2 * (p_bar * ((1 + c_bar) / (1 - c_bar) + b**2)) ** (-0.5)
 
    return S

Edge cases and nan_policy

  • When $\bar{c} = 1$ (perfectly straight): the denominator $(1 - \bar{c})$ is zero, and $S = 0$.
  • When all steps are NaN: return NaN.
  • When step length is zero (stationary): turning angle is NaN (from movement#833), which propagates correctly.
  • nan_policy should follow the convention established in compute_path_length. Niko noted on Zulip that "we may realise that the current policies don't cover what we need" — this metric may be a case where that becomes relevant.

Proposed API

def compute_sinuosity(
    data: xr.DataArray,
    nan_policy: Literal["ffill", "scale"] = "ffill",
    nan_warn_threshold: float = 0.2,
) -> xr.DataArray:

Returns: A scalar per individual/keypoint (like compute_path_length), with time and space dimensions removed.

References

Context

Opened following discussion with Niko on Zulip. Related to #406

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    🤔 Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions