Signal Tools

Digital signal processing for any numeric series — no LLM, no credits, fully deterministic.

Signal tools operate entirely in-process. There are no external calls, no AI generation, and no credit cost. The transforms (fft, ifft, spectral) run natively in O(n log n). Every response includes a deterministic receipt under _meta.datagrout, and outputs carry both values and records so results drop straight into prism.chart / prism.refract. All Signal tools are available at data-grout@1/signal.*@1.

Binary payload input

Alongside JSON arrays, every numeric input X also accepts X_b64 — a base64 string of packed little-endian IEEE-754 floats. The dtype argument selects float64 (default, 8 bytes) or float32 (4 bytes). This is a compact, exact alternative for large signals that avoids a giant JSON array. An explicit JSON array under X always wins over X_b64.

The suite covers two areas:

  • Transforms: fft, ifft, spectral — move between the time and frequency domains and read off the spectrum.
  • Filtering: filter, convolve — isolate or remove frequency bands, smooth, and apply custom kernels.

Signal inputs resolve from explicit arguments (data/real/imag/a/b) or from payload/cache_ref, so you can pipe a series from a Data/Frame tool directly in.

Frequency conventions

Cutoff frequencies for filter are normalized to the sample rate, in the range (0, 0.5], where 0.5 is the Nyquist frequency. If you have a real sample rate fs, pass cutoff = f_hz / fs. spectral and fft accept a sample_rate (Hz) and report each bin’s freq in Hz; with the default sample_rate of 1.0, frequencies are in cycles/sample.


signal.generate@1

Test signals. Types: sine, chirp (linear or log sweep from frequency to f1), square (duty in (0, 1)), triangle, noise (seeded; gaussian with std = amplitude, or uniform in ±amplitude), impulse (one sample at position). Give n samples, or duration in seconds with sample_rate. amplitude, phase (radians) and offset shape each component; components sums several (a tone in noise, two tones, a chirp on a DC offset) with the same fields per entry. Deterministic — noise takes a seed (default 1) — so a pipeline that starts here reproduces its signal and receipt. output_b64: true returns packed little-endian float64 in values_b64 instead of a JSON values, which is how a million samples should travel. Frequencies must sit below sample_rate / 2. Max 1,048,576 samples.

Parameters

Parameter Type Required Default Description
type string yes (unless components) – sine | chirp | square | triangle | noise | impulse
n / duration integer / number one of – Samples, or seconds × sample_rate
sample_rate number no 1.0 Hz
frequency number for periodic + chirp – Hz (cycles/sample at rate 1)
f1, sweep number, string chirp linear End frequency; linear | log
amplitude, phase, offset number no 1, 0, 0 Shape
duty number square 0.5 High fraction
distribution, seed string, integer noise gaussian, 1
position integer impulse 0
components object[] no – Summed; each entry takes the fields above
output_b64 boolean no false Packed output

Example

{
  "name": "data-grout@1/signal.generate@1",
  "arguments": {
    "duration": 1, "sample_rate": 8000,
    "components": [
      { "type": "sine", "frequency": 440 },
      { "type": "noise", "amplitude": 0.1, "seed": 7 }
    ]
  }
}

Response: { "count": 8000, "sample_rate": 8000.0, "duration": 1.0, "components": [{ "type": "sine", "frequency": 440.0, ... }, { "type": "noise", "seed": 7, ... }], "values": [...], "records": [...] }.


signal.fft@1

Forward Fast Fourier Transform of a real or complex signal (native, O(n log n)). Returns the complex spectrum (real, imag), magnitude, phase, and — with sample_rate — per-bin freq, plus a records list. Set one_sided: true to return only bins 0..n/2 (the non-redundant half for real input). Max 1,048,576 samples.

Parameters

Parameter Type Required Description
data number[] one of Real signal samples
real / imag number[] one of Explicit real/imaginary parts (imag defaults to zeros)
sample_rate number no Sampling rate (Hz) for per-bin frequency
one_sided boolean no Return only bins 0..n/2 (default false)

Example

{
  "name": "data-grout@1/signal.fft@1",
  "arguments": { "data": "$sensor.readings", "sample_rate": 1000, "one_sided": true }
}

Response: { "n": 1024, "real": [...], "imag": [...], "magnitude": [...], "phase": [...], "records": [{ "index": 4, "freq": 3.9, "magnitude": 512.0, ... }] }.


signal.ifft@1

Inverse FFT (native, O(n log n)). Reconstructs a time/space-domain signal from a complex spectrum, normalized by 1/n so ifft(fft(x)) == x. Returns the complex output (real, imag) and, as values, the real part — the recovered signal for spectra that came from real input. Max 1,048,576 bins.

Parameters

Parameter Type Required Description
real number[] yes Real part of the spectrum
imag number[] no Imaginary part (defaults to zeros)

Example

{
  "name": "data-grout@1/signal.ifft@1",
  "arguments": { "real": "$fft.real", "imag": "$fft.imag" }
}

Response: { "n": 8, "values": [3, 1, 4, 1, 5, 9, 2, 6], "real": [...], "imag": [...] }.


signal.spectral@1

One-sided power spectrum of a real signal, computed via the native FFT (O(n log n)). Returns, per frequency bin, the freq, magnitude, power, and phase, plus the top peaks (excluding DC) and the dominant_frequency. An optional window (hann / hamming) reduces spectral leakage. Max 1,048,576 samples.

Parameters

Parameter Type Required Default Description
data number[] yes – Real signal samples (or via payload/cache_ref)
sample_rate number no 1.0 Sampling rate (Hz); default reports cycles/sample
window string no none none | hann | hamming
top_peaks integer no 5 Number of spectral peaks to return

Example

{
  "name": "data-grout@1/signal.spectral@1",
  "arguments": { "data": "$sensor.readings", "sample_rate": 1000, "window": "hann" }
}

Response: { "n": 512, "dominant_frequency": 60.0, "peaks": [{ "freq": 60.0, "power": 812.4, ... }], "records": [...] }.


signal.stft@1

Short-time Fourier transform — a spectrogram. Slides a window_size frame across the signal by hop, applies a window function (Hann by default), and takes the one-sided power spectrum of every frame via the native FFT. Returns the spectrogram as matrix (frames × bins, one row per frame) with times (frame centres) and freqs (bin frequencies), per-frame records — dominant_freq (excluding DC), peak_power, total_power, flatness (0 for a pure tone, 1 for white noise) — and values, the dominant frequency per frame, ready for math.window or a regime rule. Long-form heatmap cells are included while frames × bins ≤ 10,000. Reach for this instead of signal.spectral whenever the signal may change over time: a whole-signal spectrum lists the components but cannot say when each was present. Max 1,048,576 samples; frames × bins ≤ 4,000,000.

Parameters

Parameter Type Required Default Description
data number[] yes – Real signal samples (or via payload/cache_ref, or data_b64)
sample_rate number no 1.0 Sampling rate (Hz); times in seconds, freqs in Hz
window_size integer no 256 Samples per frame (FFT size); frequency resolution = sample_rate / window_size
hop integer no window_size / 2 Samples between frame starts
window string no hann none | hann | hamming | blackman
scale string no power power | magnitude | db for matrix / cells

Example

{
  "name": "data-grout@1/signal.stft@1",
  "arguments": { "data": "$vibration.samples", "sample_rate": 8000, "window_size": 512, "hop": 256 }
}

Response: { "frames": 31, "bins": 257, "times": [0.032, 0.064, ...], "freqs": [0.0, 15.625, ...], "values": [125.0, 125.0, 437.5, ...], "records": [{ "index": 0, "time": 0.032, "dominant_freq": 125.0, "peak_power": 3.1e5, "total_power": 3.4e5, "flatness": 0.02 }, ...], "matrix": [[...], ...] }.


signal.iir@1

Biquad (second-order IIR) filters from the Audio EQ Cookbook: lowpass, highpass, bandpass (constant 0 dB peak), notch, peaking, lowshelf, highshelf. Give the corner as cutoff normalized to the sample rate in (0, 0.5), or as frequency in Hz with sample_rate. q shapes the transition (0.7071 = Butterworth, the default; for notch/peaking, bandwidth ≈ cutoff / q); peaking and the shelves take gain_db; stages cascades identical sections. The filter is causal (zero initial state). zero_phase: true filters forward and then backward — no phase distortion, gain applied twice, and every output sample depends on later input, so the result carries a note saying it is for offline analysis only. The one-section coefficients (a0 normalized to 1) and a 65-point magnitude response from DC to Nyquist are always returned, so the filter can be checked before its output is trusted. Prefer this over signal.filter when latency or cost matter; prefer signal.filter when you need a linear-phase (FIR) response. Max 1,048,576 samples.

Parameters

Parameter Type Required Default Description
data number[] yes – Input samples (or via payload/cache_ref, or data_b64)
type string yes – lowpass | highpass | bandpass | notch | peaking | lowshelf | highshelf
cutoff number one of – Corner / centre, normalized to the sample rate, in (0, 0.5)
frequency + sample_rate number + number one of – Corner / centre in Hz with the sampling rate; response then reports Hz
q number no 0.7071 Quality factor (Butterworth default)
gain_db number for peaking/shelves – Boost (+) or cut (−) in dB
stages integer no 1 Cascade 1–8 identical sections
zero_phase boolean no false Forward-backward filtering; offline only

Example

{
  "name": "data-grout@1/signal.iir@1",
  "arguments": { "data": "$mic.samples", "type": "notch", "frequency": 60, "sample_rate": 8000, "q": 10 }
}

Response: { "values": [...], "count": 8000, "type": "notch", "cutoff": 0.0075, "q": 10.0, "coefficients": { "b0": 0.997649, "b1": -1.99308, "b2": 0.997649, "a1": -1.99308, "a2": 0.995298 }, "response": [{ "freq": 0.0, "gain_db": 0.0 }, ...] }.


signal.hilbert@1

Analytic signal x + j·x̂ via the Hilbert transform (native FFT: negative frequencies zeroed, positive doubled, transformed back). Returns the amplitude envelope (also values) — the demodulated AM signal, a vibration or EEG amplitude track, an onset detector’s input — the transform hilbert itself, the unwrapped instantaneous phase in radians, the instantaneous frequency from the phase difference (Hz with sample_rate; null at index 0), and mean_frequency over the interior 80%. The FFT construction treats the signal as periodic, so the first and last few percent ripple: judge the interior. A DC offset adds to the envelope — pass remove_mean: true when the signal has one. Max 1,048,576 samples, minimum 4.

Parameters

Parameter Type Required Default Description
data number[] yes – Real signal samples (or via payload/cache_ref, or data_b64)
sample_rate number no 1.0 Sampling rate (Hz); instantaneous frequency in Hz
remove_mean boolean no false Subtract the mean before the transform

Example

{
  "name": "data-grout@1/signal.hilbert@1",
  "arguments": { "data": "$bearing.accel", "sample_rate": 8000, "remove_mean": true }
}

Response: { "n": 8000, "envelope": [...], "hilbert": [...], "phase": [...], "frequency": [null, 118.7, ...], "mean_frequency": 120.1, "records": [{ "index": 0, "envelope": 0.98, "phase": 0.12, "frequency": null }, ...] }.


signal.resample@1

Rational sample-rate conversion with anti-aliasing. Give integer factors up and down (reduced to lowest terms), or sample_rate and to_rate in Hz (the ratio is formed and reduced for you — 44 100 → 48 000 becomes 160/147). Conceptually: insert up − 1 zeros between samples, lowpass at half the narrower Nyquist with a Hamming-windowed sinc, keep every down-th sample; the three steps are fused so each output sample reads only the input under the kernel. Output length is ceil(n · up / down). Beyond the ends the signal is treated as zero, so edge_samples output samples at each end carry the filter’s transient. There is deliberately no “keep every k-th sample” or linear-interpolation mode: both alias, which is the problem this tool exists to avoid. Max 1,048,576 samples in, 4,000,000 out, factors up to 1024 (resample in two steps for more).

Parameters

Parameter Type Required Default Description
data number[] yes – Input samples (or via payload/cache_ref, or data_b64)
up + down integer + integer one of – Integer factors; either may be omitted (defaults to 1)
sample_rate + to_rate number + number one of – Input and target rates in Hz
label string no value Renames the value key in records

Example

{
  "name": "data-grout@1/signal.resample@1",
  "arguments": { "data": "$imu.accel_z", "sample_rate": 250, "to_rate": 100 }
}

Response: { "count": 4000, "input_count": 10000, "up": 2, "down": 5, "sample_rate": 250.0, "output_rate": 100.0, "taps": 101, "edge_samples": 25, "values": [...] }.


signal.peaks@1

Find peaks in a time series — the counterpart of signal.spectral‘s peaks, in time rather than frequency. A local maximum is a sample strictly above its left neighbour and at least its right one; a flat top counts once, at its middle. Filters follow scipy.signal.find_peaks: height (minimum value), distance (minimum samples between peaks — of two closer than that, the lower is dropped), prominence (a peak’s rise above the higher of the two valleys between it and the nearest higher samples on each side — the filter that separates an event from ripple on a slope). Each peak returns index, height, prominence, left_base / right_base, and width at half prominence (interpolated). valleys: true finds minima, reported in the original sign. With sample_rate, each peak also has time, and the result carries mean_interval and rate in peaks per second (× 60 for bpm or rpm). Max 1,048,576 samples, minimum 3.

Parameters

Parameter Type Required Default Description
data number[] yes – Series samples (or via payload/cache_ref, or data_b64)
height number no – Keep peaks at or above this (valleys: at or below)
distance integer no – Minimum samples between kept peaks
prominence number no – Minimum prominence
valleys boolean no false Find minima instead
sample_rate number no – Hz; adds time per peak and rate

Example

{
  "name": "data-grout@1/signal.peaks@1",
  "arguments": { "data": "$ecg.lead2", "sample_rate": 250, "prominence": 0.6, "distance": 50 }
}

Response: { "count": 25, "indices": [50, 150, ...], "rate": 2.5, "mean_interval": 0.4, "records": [{ "index": 50, "time": 0.2, "height": 1.02, "prominence": 0.98, "left_base": 31, "right_base": 68, "width": 4.7 }, ...] }.


signal.surrogate@1

Surrogate-data test for a detected cycle. Builds n phase-randomised surrogates of the whole record — FFT, keep every magnitude, draw new uniform phases (Hermitian symmetry preserved), inverse FFT — so each surrogate has exactly the record’s power spectrum and autocorrelation but none of its time-localised structure. That is the null a regime or cycle claim has to beat. Because the whole-record spectrum is preserved exactly, the statistic is local: the peak non-DC power of the spectrum of a window_size segment, by default the last one (the window a detection was made in). The original’s statistic is ranked against the surrogates’: rank 1 means no surrogate matched it, p_value = rank / (n + 1) one-sided, z_score its distance in surrogate standard deviations, and interpretation says it in words. Significant → the cycle is a feature of that window, not of the record. A tone running through the whole record is deliberately not flagged: the record’s spectrum already explains it. values is the null distribution, ready for math.describe. Seeded (default 1) for reproducible receipts. window_size ≥ n is refused with the reason. Max 262,144 samples, 200 surrogates.

Parameters

Parameter Type Required Default Description
data number[] yes – The whole record (or via payload/cache_ref, or data_b64)
window_size integer no min(256, n/2) Segment length for the statistic; must be < n
segment_start integer no n − window_size First sample of the segment
n integer no 19 Surrogates: 19 → p = 0.05 at rank 1; 99 → p = 0.01
seed integer no 1 RNG seed
window string no hann Window on the segment: none | hann | hamming | blackman
sample_rate number no 1.0 Hz for dominant_frequency

Example

{
  "name": "data-grout@1/signal.surrogate@1",
  "arguments": { "data": "$bars.log_return", "window_size": 256, "n": 99, "sample_rate": 1 }
}

Response: { "statistic": 41.2, "dominant_frequency": 0.0625, "surrogate_mean": 12.8, "surrogate_std": 4.1, "z_score": 6.9, "rank": 1, "p_value": 0.01, "significant": true, "values": [...], "interpretation": "No surrogate out of 99 reached the segment's peak power: ..." }.


signal.filter@1

FIR frequency filter built from a windowed-sinc kernel (Hamming window), convolved with the signal to give a same-length output. Types: lowpass, highpass, bandpass, bandstop. lowpass/highpass take a single cutoff; bandpass/bandstop take low and high. All cutoffs are normalized to (0, 0.5].

A linear-phase FIR delays what it passes by (num_taps − 1) / 2 samples. The default output hides that delay by centring the kernel, so each output sample uses (num_taps − 1) / 2 samples of future input — correct for offline analysis, look-ahead in anything live. The result reports this as look_ahead_samples with a note. Pass causal: true to use past samples only: the output is then delayed by delay_samples and never depends on input that has not arrived. valid_from is the first output index clear of the start-up transient in either mode. For a low-latency live path prefer signal.iir. Max 100,000 samples, 2,001 taps.

Parameters

Parameter Type Required Default Description
data number[] yes – Input signal samples
type string yes – lowpass | highpass | bandpass | bandstop
cutoff number low/high – Normalized cutoff for lowpass/highpass
low, high number band – Normalized band edges (low < high)
num_taps integer no 51 FIR kernel length (forced odd); higher = sharper transition
causal boolean no false Past samples only; output delayed by (num_taps − 1) / 2

Example

{
  "name": "data-grout@1/signal.filter@1",
  "arguments": { "data": "$sensor.readings", "type": "lowpass", "cutoff": 0.05, "causal": true }
}

Response: { "values": [...], "records": [...], "count": 1000, "type": "lowpass", "num_taps": 51, "causal": true, "delay_samples": 25, "look_ahead_samples": 0, "valid_from": 50 }. Without causal: "delay_samples": 0, "look_ahead_samples": 25, "valid_from": 25, "note": "zero-phase output: each sample uses 25 future input samples …".


signal.convolve@1

Discrete convolution of two numeric sequences (a * b). Modes: full (length len(a)+len(b)-1, default), same (length len(a), centered), valid (overlap only). Useful for smoothing, feature detection, and applying custom kernels. Max 50,000 per input (and ≤ 5,000,000 total operations).

Parameters

Parameter Type Required Default Description
a number[] yes – First sequence (the signal)
b number[] yes – Second sequence (the kernel)
mode string no full full | same | valid

Example

{
  "name": "data-grout@1/signal.convolve@1",
  "arguments": { "a": [1, 2, 3], "b": [0, 1, 0.5], "mode": "full" }
}

Response: { "values": [0, 1, 2.5, 4, 1.5], "records": [...], "count": 5, "mode": "full" }.



signal.correlate@1

Cross-correlation of two sequences by lag — the DSP sliding dot-product, not the Pearson coefficient (that’s math.correlate). Omit b and it defaults to a, giving the auto-correlation (peaks away from lag 0 mark repeat intervals — periodicity detection). Set normalize: true to scale by the signal energies so values fall in [-1, 1] (auto-correlation is then 1.0 at lag 0). Modes: full (default), same, valid. Returns values, {lag, value} records, and the peak_lag. Max 50,000 per input.

Statistical modes (a only): mode: "acf" is the sample autocorrelation function — mean removed, normalized so lag 0 is 1, from lag 0 to max_lag (default min(n − 1, floor(10·log10 n)), the statsmodels convention). mode: "pacf" is the partial autocorrelation by Durbin–Levinson, the plot an AR order is read from (the last lag outside the band). Both return band = 1.96/√n (the two-sided 95% band under white noise), significant_lags (lags ≥ 1 whose |value| exceeds it), first_significant_lag, and a significant flag on each record.

Parameters

Parameter Type Required Default Description
a number[] yes – First sequence
b number[] no a Second sequence; omit for auto-correlation (must be omitted for acf/pacf)
mode string no full full | same | valid | acf | pacf
max_lag integer no statsmodels default acf/pacf: highest lag reported (1 ≤ max_lag < n)
normalize boolean no false Scale to [-1, 1] by signal energies (sliding modes)

Example

{
  "name": "data-grout@1/signal.correlate@1",
  "arguments": { "a": "$bars.log_return", "mode": "pacf", "max_lag": 20 }
}

Response: { "values": [1.0, 0.71, 0.02, -0.03, ...], "records": [{ "lag": 1, "value": 0.71, "significant": true }, ...], "band": 0.031, "significant_lags": [1], "first_significant_lag": 1, "n": 4000, "max_lag": 20, "mode": "pacf" }.

Sliding-mode example: { "a": "$sensor.readings", "normalize": true } → { "values": [...], "records": [{ "lag": -6, "value": 0.12 }, ...], "peak_lag": 0, "count": 13 }.


Composition

Pull a series through Data / Frame tools, transform it here, then chart the spectrum or waveform with Prism Chart — the records (frequency, magnitude, phase) and filtered values are chart-ready. Pair with Math for descriptive statistics on the magnitudes, or with Linalg when spectral features feed clustering or similarity. A common pipeline is fft → edit the spectrum → ifft for frequency-domain processing.