concord-metrics-univariate

Time-domain, single-channel features: Line Length and Hjorth Parameters

Role in the system Implements two classic EEG feature metrics. Each is a stateless class implementing the Metric ABC. They use metrics-utils for windowing and produce MetricResult containers.

Package Structure

concord-metrics-univariate/
  src/concord_metrics_univariate/
    __init__.py       exports LineLength, HjorthParameters
    line_length.py    LineLength metric
    hjorth.py         HjorthParameters metric

LineLength

Line length is one of the simplest and most effective seizure detection features. It measures how "wiggly" the signal is — how much total variation there is per unit time. High line length indicates fast, high-amplitude activity characteristic of seizures.

Formula: LL = Σ |x[n] − x[n-1]| — the sum of absolute first differences.

classLineLength(window_s=None, step_s=None)
ParameterDefaultDescription
window_sNoneWindow length in seconds. If None, computes over the entire recording.
step_sNoneStep between windows. If None, defaults to window_s (non-overlapping). Ignored if window_s is None.

name: "line_length"

units: "V" (volts, since it's a sum of differences of voltage)

Output shape

Modedata.shapetime_axis
window_s = None (global)(n_channels,)None
window_s set(n_channels, n_windows)(n_windows,) — center time of each window

How it works

# Windowed mode (simplified):
windows = segment(data, fs, window_s, step_s)  # (n_ch, n_win, win_samples)
diff = np.diff(windows, axis=-1)               # (n_ch, n_win, win_samples-1)
ll = np.sum(np.abs(diff), axis=-1)             # (n_ch, n_win)

HjorthParameters

Hjorth parameters are three complementary descriptors proposed by Bo Hjorth (1970) that characterize a signal's temporal complexity. They are computed from the signal and its successive derivatives.

ParameterFormulaPhysical interpretation
Activity var(x) Signal power (variance). High in high-amplitude bursts.
Mobility std(x') / std(x) Mean frequency of the signal. Proportional to the dominant frequency.
Complexity mobility(x') / mobility(x) How closely the signal resembles a pure sine wave. 1.0 = pure sine, higher = more complex/noisy.

Here, x' is the first derivative (approximated as successive differences), and x'' the second derivative.

classHjorthParameters(window_s=None, step_s=None)
ParameterDefaultDescription
window_sNoneWindow length in seconds. If None, computes globally.
step_sNoneStep between windows.

name: "hjorth_parameters"

Degenerate channels: If a channel is constant (std=0), all three parameters are set to NaN rather than raising an error or producing Inf.

Output shape

Modedata.shapeAxis 2 contents
window_s = None(n_channels, 3)[activity, mobility, complexity]
window_s set(n_channels, n_windows, 3)[activity, mobility, complexity] per window

The frontend accesses individual parameters by index: data[:, :, 0] = activity, data[:, :, 1] = mobility, data[:, :, 2] = complexity.

How it works

def _hjorth_on_array(x):
    # x: (..., n_samples) — last axis is time
    var_x   = np.var(x, axis=-1)
    dx      = np.diff(x, axis=-1)
    var_dx  = np.var(dx, axis=-1)
    ddx     = np.diff(dx, axis=-1)
    var_ddx = np.var(ddx, axis=-1)

    activity   = var_x
    mobility   = np.sqrt(var_dx / var_x)
    complexity = np.sqrt(var_ddx / var_dx) / mobility
    return np.stack([activity, mobility, complexity], axis=-1)

Example Usage

from concord_io import read_bids_ieeg
from concord_metrics_univariate import LineLength, HjorthParameters

rec = read_bids_ieeg("sub-HUP117_run-01_ieeg.edf")

# Line length: 1-second windows, no overlap
ll = LineLength(window_s=1.0).compute(rec)
print(ll.data.shape)     # (72, 300)  — 72 channels, 300 one-second windows
print(ll.time_axis[:3])  # [0.5, 1.5, 2.5]  — center times

# Hjorth: 2-second windows, 1-second step (50% overlap)
hjorth = HjorthParameters(window_s=2.0, step_s=1.0).compute(rec)
print(hjorth.data.shape)        # (72, 299, 3)
activity   = hjorth.data[:, :, 0]   # (72, 299)
mobility   = hjorth.data[:, :, 1]
complexity = hjorth.data[:, :, 2]