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.