Skip to content

Signal

signal

Array-pure signal utilities.

Numeric helpers that operate on plain NumPy arrays sampled along an axis — no :class:~pybvh.bvh.Bvh involved. The centerpiece is :func:finite_difference, the single derivative convention shared by the kinematics ladder (:mod:pybvh.analysis) and the geometry derivative kernels (:mod:pybvh.geometry); the rest are self-contained statistics, smoothing, spectrum, and simplification tools (no scipy).

finite_difference(arr: npt.NDArray[np.float64], dt: float, *, stencil: str = 'central', pad: str = 'edge', axis: int = 0) -> npt.NDArray[np.float64]

Differentiate a sampled array along one axis.

The single finite-difference convention shared across pybvh — the kinematics ladder (node_velocities…accelerations → jerk) and the geometry derivative kernels (curvature, torsion, movement_phase) all route through this, so derivatives composed across the two stay consistent.

Parameters:

Name Type Description Default
arr ndarray

Samples taken at a uniform step dt along axis.

required
dt float

Sample spacing (e.g. frame_time).

required
stencil ('central', 'forward')

"central" (default): np.gradient — second-order accurate interior, one-sided at the boundary. "forward": (arr[i+1] - arr[i]) / dt, first-order, causal.

"central"
pad ('edge', 'none')

"edge" (default): output keeps the input length along axis. "none": drop the boundary samples the stencil cannot define — central drops one at each end, forward drops the trailing one.

"edge"
axis int

Axis to differentiate along (default 0, the frame axis).

0

Returns:

Type Description
ndarray

The derivative. Same shape as arr when pad="edge"; shorter by 2 (central) or 1 (forward) along axis when pad="none".

Raises:

Type Description
ValueError

If stencil or pad is invalid.

temporal_stats(signal: npt.NDArray[np.float64], axis: int = 0) -> TemporalStats

Summary statistics of a signal along an axis.

Returns mean, std, min, max, and the third/fourth standardized moments (skewness and excess kurtosis), all reduced over axis. Skew and kurtosis are computed by hand (no scipy). Where the std is ~0 (a constant signal) skewness and kurtosis are nan.

All estimators are the population (biased) forms: std uses ddof=0, and the moments are plain 1/N sums, giving g₁ = m₃/s³ and g₂ = m₄/s⁴ − 3. The bias-corrected sample forms — what pandas.Series.skew() / .kurt() / .std() return, and what scipy gives with bias=False — carry extra N-dependent factors and differ materially on short signals (converging as N grows). Kurtosis is excess (normal ⇒ 0), not raw (normal ⇒ 3). The population convention matches cov3dj and the rest of pybvh's descriptors, which treat a clip as the whole population rather than a sample of one.

Parameters:

Name Type Description Default
signal ndarray

Input signal.

required
axis int

Axis to reduce over (default 0).

0

Returns:

Type Description
TemporalStats

Named tuple (mean, std, min, max, skewness, kurtosis) with axis removed.

box_filter_smooth(signal: npt.NDArray[np.float64], window: int, axis: int = 0) -> npt.NDArray[np.float64]

Moving-average smoothing with a box kernel of width window.

Edge samples are handled by edge-padding (the alternatives — reflect, zero-pad, or a shrinking window at the ends — bias the first and last samples differently) so the output keeps the input length. Fully vectorized via a cumulative-sum sliding window (no Python loop over the signal).

An even window cannot be centered on a sample; this takes the extra sample from the future ((window-1)//2 back, window//2 forward), so the output leads the input by half a sample. The mirror choice lags by the same amount. Use an odd window when that half-sample matters, or when matching another implementation's even-window output.

Parameters:

Name Type Description Default
signal ndarray

Input signal.

required
window int

Box width in samples (>= 1). window == 1 is a no-op.

required
axis int

Axis to smooth along (default 0).

0

Returns:

Type Description
ndarray

The smoothed signal, same shape as signal.

Raises:

Type Description
ValueError

If window < 1.

fft_magnitude(signal: npt.NDArray[np.float64], fs: float = 1.0, axis: int = 0, *, norm: str = 'backward') -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]

One-sided FFT magnitude spectrum of a real signal.

The default is unnormalized — the raw |rfft(signal)|, with no rectangular window correction and no scaling. Raw magnitude scales with signal length, so values are comparable across bins and across signals of the same length only; select a normalization via norm before comparing spectra of different lengths or against published amplitudes. :func:sparc normalizes by its own peak, so this choice does not affect it.

Parameters:

Name Type Description Default
signal ndarray

Real input signal.

required
fs float

Sampling rate in Hz (default 1.0).

1.0
axis int

Axis to transform along (default 0).

0
norm ('backward', 'ortho', 'forward', 'amplitude')

Normalization of the returned magnitude. The first three follow numpy's rfft vocabulary: "backward" (default) applies no scaling — the raw |rfft|; "ortho" divides by √N; "forward" divides by N. "amplitude" is the single-sided amplitude spectrum the signal-processing literature plots — 2|X|/N, with the DC bin (and the Nyquist bin, for even N) not doubled, since those frequencies have no negative-frequency twin to fold in — so a pure sine of amplitude A peaks at A. Note "amplitude" is not one of numpy's norms: numpy's "forward" lacks the one-sided doubling, so it reads half the sine's amplitude.

"backward"

Returns:

Name Type Description
freqs (ndarray, shape(T // 2 + 1))

Non-negative frequency bins in Hz.

magnitude ndarray

|rfft(signal)| along axis, scaled per norm.

Raises:

Type Description
ValueError

If norm is not one of the four options.

dominant_frequency(signal: npt.NDArray[np.float64], fs: float, axis: int = 0) -> npt.NDArray[np.float64]

Frequency (Hz) of the largest non-DC spectral component.

The DC bin is excluded so a non-zero mean doesn't dominate.

The peak is the argmax of the raw spectrum at native bin resolution fs / T: no zero-padding, no window, no sub-bin interpolation. Resolution is therefore fs / T, and rectangular-window leakage can move the winning bin for a tone falling between bins — pad the signal or use quadratic peak interpolation if you need better than bin precision. Exact ties go to the lowest frequency (np.argmax).

Parameters:

Name Type Description Default
signal ndarray

Real input signal.

required
fs float

Sampling rate in Hz.

required
axis int

Axis to analyze along (default 0).

0

Returns:

Type Description
ndarray

Dominant frequency with axis removed (a scalar for 1-D input).

ramer_douglas_peucker(curve: npt.NDArray[np.float64], eps: float) -> npt.NDArray[np.float64]

Simplify a polyline with the Ramer–Douglas–Peucker algorithm.

Drops points that lie within eps of the simplified path, keeping the overall shape. Operates on a single curve (recursion over the curve's own points, not over a batch); each split's perpendicular distances are computed vectorized.

Parameters:

Name Type Description Default
curve (ndarray, shape(P, D))

Ordered points of one curve (any dimension D).

required
eps float

Maximum allowed perpendicular deviation.

required

Returns:

Type Description
(ndarray, shape(K, D))

The retained points (endpoints always kept), in order.