Wavelet transforms.
AI & ML features.

High-performance wavelet transforms in pure Rust. Classical and adaptive decompositions, streaming primitives at nanosecond latency, and feature extraction for AI & ML pipelines — all numerically equivalent to PyWavelets at 1e-10.

12 transforms
6 classical · 6 adaptive — DWT to VMD
5 wavelet families
Haar · Daubechies · Symlet · Coiflet · Biorthogonal
1,900+ tests
Pinned to PyWavelets · scipy
125 theorems
Machine-checked in Lean — no sorry
~67 ns
Streaming MODWT / tick
15–20×
FFT-accelerated CWT

The wavelet canon, in Rust

Twelve transforms across two categories — six classical wavelet transforms (DWT, MODWT, SWT, WPT, CWT, dual-tree CWT) and six adaptive decomposition methods (EMD, CEEMDAN, MEMD, VMD, EWT, SST) plus the Hilbert-Huang downstream. Five wavelet families implemented (Haar, Daubechies, Symlets, Coiflets, Biorthogonal CDF) plus a Wavelet trait for custom filters.

ψ

Five wavelet families

Orthogonal, biorthogonal, and complex wavelets straight from the canon. Bring your own via the Wavelet trait.

  • Haar
  • Daubechies Db2 — Db10
  • Symlets Sym2 — Sym10
  • Coiflets Coif1 — Coif5
  • Biorthogonal CDF 5/3 and 9/7
  • Custom filters via Wavelet trait
DWT

Classical wavelet transforms

Discrete and continuous wavelet transforms across the standard family — orthogonal, shift-invariant, packet-decomposed, and complex.

  • DWT, IDWT, multilevel (PyWavelets-canonical output lengths)
  • MODWT, IMODWT (shift-invariant)
  • SWT, ISWT (stationary)
  • WPT, IWPT with best-basis selection
  • CWT, FFT-based CWT, ICWT
  • Complex wavelets + dual-tree CWT
EMD

Adaptive decomposition family

Data-adaptive decompositions for non-stationary signals — provable AM-FM convergence (SST), exact reconstruction (CEEMDAN, EWT), and cross-channel mode alignment (MEMD).

  • EMD, EEMD — empirical mode decomposition
  • CEEMDAN — Improved (ICEEMDAN) variant default
  • MEMD — multivariate, cross-channel alignment
  • VMD — variational mode decomposition via ADMM
  • EWT — Fourier-adaptive Meyer filter bank
  • SST/SSWT — synchrosqueezed time-frequency ridges
  • Hilbert Spectrum / HHT — productized end-to-end

Wavelet features for AI & ML pipelines

Four capability shapes for ML workflows: multi-scale feature vectors for classical models, scaleogram tensors for CNNs, streaming features for online inference, and bit-equivalent Python→Rust portability through PyWavelets pinning.

FV

Tabular feature vectors

Shift-invariant modwt_multilevel gives per-level variance, energy, and arbitrary statistics across J decomposition levels. Pair with estimate_hurst_exponent (wavelet-leader Hurst with bootstrap CIs) for a compact tabular feature vector per signal window — ready for scikit-learn, XGBoost, or any tabular learner.

2D

Scaleogram tensors for CNNs

cwt_fft and sswt produce 2-D time-frequency representations (n_scales × n_time) that feed directly into image-CNN architectures — the standard shape for biomedical signal classification, bearing-fault detection, and acoustic event recognition. Complex Morlet magnitude or SSWT power both work as tensor inputs.

RT

Streaming features for online inference

StreamingMODWTat ~67 ns per tick produces new feature coefficients on every sample — the right shape for online-learning loops, real-time inference services, and per-tick anomaly detectors that can't afford to re-batch a window on every update.

PY

Python prototype, Rust production

Every coefficient is pinned to PyWavelets at 1e-10 in the test suite. A feature pipeline prototyped against PyWavelets in Python (with scikit-learn, PyTorch, or JAX) ports to ferro-wave in Rust with the same numerical features — no retraining for numerical drift, no FFI hop in the hot path.

use ferro_wave::{Signal, Daubechies, DaubechiesType};
use ferro_wave::transform::modwt_multilevel;
use ferro_wave::analysis::multifractal::estimate_hurst_exponent;

// Per-window feature vector for downstream ML.
fn features(window: &[f64]) -> ferro_wave::Result<Vec<f64>> {
    let signal  = Signal::new(window.to_vec());
    let wavelet = Daubechies::new(DaubechiesType::Db4);

    let coeffs = modwt_multilevel(&signal, &wavelet, 5)?;
    let mut feats: Vec<f64> = coeffs.wavelet_coeffs.iter()
        .flat_map(|d| {
            let n    = d.len() as f64;
            let mean = d.data().iter().sum::<f64>() / n;
            let var  = d.data().iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
            [var, d.energy_fast()]   // per-level: variance + energy
        })
        .collect();

    let hurst = estimate_hurst_exponent(&signal, &wavelet, None)?;
    feats.push(hurst.hurst_exponent);   // multifractal regime descriptor

    Ok(feats)   // 11 features per window · bit-equivalent to PyWavelets
}

How a transform runs

Four layers, each with an explicit contract. The signal layer owns boundary semantics; the transform layer owns the wavelet; the analysis layer reads coefficients only; the reconstruction layer closes the round-trip.

1

Signal

Signal<f64> from a slice or streaming source. Boundary modes — periodic, symmetric, zero-pad, reflect — explicit at the API surface.

2

Decompose

Run DWT, MODWT, SWT, WPT, or CWT over the signal with a chosen wavelet. SIMD-vectorized inner loops on supported platforms.

3

Analyze

Read the coefficients: denoise, wavelet coherence, multifractal/Hurst, matching pursuit. The analysis layer never touches the raw signal.

4

Reconstruct

IDWT, IMODWT, ISWT, IWPT, ICWT close the round-trip. Orthogonal-wavelet round-trips pin to 1e-10; ICWT correlation runs ≥ 0.987 across every wavelet × signal pair in the test suite.

Tick-by-tick, without batching

Five low-latency streaming primitives — StreamingDWT, StreamingMODWT, StreamingSWT, StreamingCWT, StreamingDenoiser — each emitting new coefficients per update(sample). Latency: ~67 ns per tick for StreamingMODWT(Db4, 4 levels), 100–600 ns for StreamingCWTacross 10–100 scales, 42 ns p50 for StreamingDenoiser (Db4, 4 levels). State stays in a bounded ring buffer; triggers fire per-tick, per-N samples, or on a wall-clock cadence. The full adaptive decomposition family — EMD, EEMD, CEEMDAN, MEMD, VMD, SSWT, EWT — also streams over a sliding window through one shared IncrementalTransform contract, bit-identical to the batch call at window-fill and on every recompute tick.

use ferro_wave::streaming::StreamingMODWT;
use ferro_wave::wavelets::{Daubechies, DaubechiesType};

let wavelet = Box::new(Daubechies::new(DaubechiesType::Db4));
let mut stream = StreamingMODWT::<f64>::new(wavelet, window, 4);

for &tick in &feed {
    if let Some(update) = stream.update(tick)? {
        on_update(update);   // per-tick coefficients
    }
}

Eight workflows on one transform path

Each card below is a 10–20 line snippet against the ferro_wave public API. Inputs are signal slices; outputs are typed results — denoised samples, streaming MODWT coefficients, jump indices, multi-scale variance, a HurstResult, a streaming Morlet scaleogram, and EMD IMFs. The same IMF contract is shared by emd, eemd, ceemdan, memd, vmd, ewt, and sswt.

01 · Denoise

Strip noise without softening the jumps

denoise_modwt runs a shift-invariant MODWT, applies universal soft-thresholding on the detail coefficients (Donoho-Johnstone), and reconstructs. Unlike a moving-average filter, threshold-then-reconstruct preserves discontinuities — jumps and regime shifts come through intact while gaussian micro-noise gets attenuated by 6–12 dB on typical tick data.

use ferro_wave::{Signal, Daubechies, DaubechiesType};
use ferro_wave::analysis::denoising::{denoise_modwt,
             DenoiseConfig, ThresholdMethod, ThresholdRule};

let signal = Signal::from_slice(&tick_returns);
let wavelet = Daubechies::new(DaubechiesType::Db4);

let config = DenoiseConfig {
    method: ThresholdMethod::Soft,
    rule:   ThresholdRule::Universal,
    levels: 4,
    ..DenoiseConfig::default()
};

let clean = denoise_modwt(&signal, &wavelet, config)?;
// clean.len() == signal.len()  — no padding artefacts
// Jumps preserved; gaussian micro-noise attenuated
Read the full guide →
MODWT denoise · Db4 · 4 levels · universal soft threshold
02 · Streaming

Update coefficients tick-by-tick

StreamingDenoiser emits one denoised sample per update(tick) call from a bounded ring of MODWT coefficients. Per-tick cost is O(filter_length × levels); measured at 42 ns p50 on a single core for Db4 at 4 levels. Steady-state allocation is zero.

use ferro_wave::{Daubechies, DaubechiesType, BoundaryMode};
use ferro_wave::streaming::denoising::{StreamingDenoiser,
             StreamingDenoiseConfig, ThresholdMethod, ThresholdRule};

let wavelet = Daubechies::new(DaubechiesType::Db4);
let config: StreamingDenoiseConfig<f64> = StreamingDenoiseConfig {
    levels: 4,
    method: ThresholdMethod::Soft,
    rule:   ThresholdRule::Universal,
    boundary: BoundaryMode::Periodic,
    ..StreamingDenoiseConfig::default()
};

let mut denoiser = StreamingDenoiser::new(&wavelet, config)?;

for &tick in &live_feed {
    let out = denoiser.update(tick)?;
    on_clean(out.clean);   // emit to strategy
}
Read the full guide →
Streaming denoise · Db4 · 4 levels · tick-by-tick
03 · Jump Detection

Spot price discontinuities with Haar detail

The Haar wavelet's detail coefficients spike sharply at discontinuities — exactly the shape of an overnight gap, a news-driven jump, or a regime shift. Threshold the finest detail level against a robust noise estimate (MAD × √(2 log N)) and you have a discrete jump locator with O(N) compute and no parameter tuning.

use ferro_wave::{Signal, Haar, BoundaryMode, dwt};
use ferro_wave::analysis::denoising::{estimate_noise_mad,
             compute_threshold, ThresholdRule};

let signal = Signal::from_slice(&prices);
let coeffs = dwt(&signal, &Haar::new(), BoundaryMode::Periodic)?;

// MAD-robust noise on finest detail
let sigma     = estimate_noise_mad(&coeffs.detail);
let threshold = compute_threshold(
    &coeffs.detail, sigma, ThresholdRule::Universal,
    signal.len());   // universal uses N, not detail.len()

let jumps: Vec<usize> = coeffs.detail.iter()
    .enumerate()
    .filter(|(_, &d)| d.abs() > threshold)
    .map(|(i, _)| i * 2)   // Haar stride
    .collect();
Read the full guide →
Haar detail · MAD threshold · jump indices
04 · Multi-scale

Decompose variance by frequency band

dwt_multilevelseparates a signal into one approximation plus N detail bands — each band concentrating variance from a distinct frequency octave. The sum of band variances equals the signal's total variance (Parseval), so you get a clean attribution: how much of your realized volatility lives at intraday scale vs daily vs weekly?

use ferro_wave::{Signal, Daubechies, DaubechiesType,
             BoundaryMode, dwt_multilevel};

let signal  = Signal::from_slice(&log_returns);
let wavelet = Daubechies::new(DaubechiesType::Db4);

let decomp = dwt_multilevel(
    &signal, &wavelet, 5,
    BoundaryMode::Periodic,
)?;

// Variance per band — sums to signal.variance()
let bands: Vec<f64> = decomp.details.iter()
    .map(|d| variance(d))
    .collect();

// D1 ≈ intraday · D2-3 ≈ daily · D4-5 ≈ weekly+
Read the full guide →
dwt_multilevel · Db4 · 5 levels · variance attribution
05 · Regime

Classify trending vs mean-reverting with Hurst

estimate_hurst_exponent fits a power-law slope to wavelet-leader scaling and returns a typed HurstResultwith the Hurst exponent and confidence interval. H ≈ 0.5 is martingale (Brownian); H < 0.5 mean-reverts; H > 0.5 trends. Track H over a rolling window and you have a regime classifier that doesn't need labelled training data. HurstResult surfaces saturated = truewhen an integrated input (prices, log-prices) pins the estimator at the boundary instead of silently returning H≈1.0.

use ferro_wave::{Daubechies, DaubechiesType};
use ferro_wave::analysis::multifractal::{
             estimate_hurst_exponent, HurstResult, MultifractalConfig};

let wavelet = Daubechies::new(DaubechiesType::Db4);
let config  = MultifractalConfig::for_hurst_only();

let h: HurstResult = estimate_hurst_exponent(
    &log_returns, &wavelet, Some(config))?;

if h.saturated {
    // integrated input (prices, log-prices) — not a regime signal
    skip_regime_switch();
} else {
    match h.hurst_exponent {
        x if x < 0.45 => switch_to_mean_revert(),
        x if x > 0.55 => switch_to_trend_follow(),
        _ => keep_neutral(),
    }
}

// h.confidence_interval — half-width at config.confidence_level
// h.r_squared           — log-log fit quality
Read the full guide →
Wavelet-leader Hurst · 95% CI · regime threshold
06 · Time-Frequency

Update a Morlet scaleogram tick-by-tick

The Continuous Wavelet Transform produces a 2D map of wavelet power across (time × scale). Two entry points: cwt_fft(signal, scales, wavelet) for full-series FFT-accelerated analysis (15–20× faster than the direct path; within 1.1–2× of fCWT C++), and StreamingCWT for per-sample updates via cascaded biquad filters. Each update(sample)call returns a fresh (power, phase) column across the configured scale set; per-tick cost is O(filter_order × scales), ~100–600 ns for 10–100 scales.

use ferro_wave::streaming::cwt::{StreamingCWT,
             FilterApproximation};

let scales: Vec<f64> = (0..32)
    .map(|i| 2.0_f64.powf(i as f64 / 4.0))
    .collect();              // 32 log-spaced

let mut cwt = StreamingCWT::try_new(
    &scales,
    1_000.0,                 // Hz
    6.0,                     // Morlet ω₀
    FilterApproximation::Biquad,
);

for &sample in &tick_feed {
    let col = cwt.update(sample);
    on_scaleogram_column(col);  // 32 (power, phase)
}
Read the full guide →
StreamingCWT · 32 log-spaced scales · Morlet ω₀=6
07 · Adaptive

Decompose non-stationary signals into IMFs with EMD

Where DWT projects a signal onto a fixed wavelet basis, Empirical Mode Decomposition lets the data pick the basis. emdsifts the signal into Intrinsic Mode Functions — one local-frequency component each — until a monotonic residual remains. It's the entry point into the adaptive family (EEMD, CEEMDAN, MEMD, VMD, EWT, sswt), all sharing the IMF result type so downstream code works unchanged across the family.

use ferro_wave::Signal;
use ferro_wave::transform::{emd, EMDConfig, IMF};

let signal = Signal::new(log_returns.to_vec());
let config = EMDConfig::default()
    .with_max_imfs(6)
    .with_max_sifting_iterations(50);

let result = emd(&signal, &config)?;

for (i, imf) in result.imfs.iter().enumerate() {
    // imf.data — same length as signal
    // imf is band-limited around one local freq
}
// result.residual — monotonic trend
// result.orthogonality_index() — Huang 1998 Eq. 5.5
// result.mode_mixing_index() — per-IMF spectral overlap

// Drop in CEEMDAN, MEMD, VMD, EWT, or SST for
// the same IMF contract over a different decomposer.
Read the full guide →
EMD · 6 IMFs + residual · CEEMDAN / MEMD / VMD / EWT / SST share the IMF contract
08 · Jump Prior

Hand a pricing engine a jump prior it can calibrate against

Detecting jumps is one thing; parameterizing them is what an option-pricing calibrator actually needs. jump_diffusion_prior_signal aggregates the same wavelet detections into a physical-measure Merton triple — λ, μ_J, σ_J — with estimator-derived standard errors, so a triple fitted from six jumps stays visibly wider than one from six hundred. Because a threshold detector only sees jumps above its own cutoff, λ is then truncation-corrected: the detected sizes are refitted as an outer-truncated normal and λ is inflated by the fitted 1/p. The payload serializes field-for-field onto FerroRisk's JumpAssumption; FerroRisk owns the P → Q Esscher tilt, so only the physical measure is ever emitted here.

use ferro_wave_finance::{jump_diffusion_prior_signal,
             FinanceProfile, JumpDiffusionPriorConfig};

// Daily closes; the profile carries the annualization
// factor — intraday profiles without one are rejected.
let profile = FinanceProfile::daily_eod();
let signal  = jump_diffusion_prior_signal(
    &closes, &profile,
    JumpDiffusionPriorConfig::for_daily_eod(),
)?;

// A payload is only ever a real estimate; a refusal
// carries a status + diagnostic instead.
if let Some(r) = signal.payload {
    let p = &r.prior;
    // p.intensity λ · p.mean μ_J · p.std σ_J
    // p.uncertainty — count + per-parameter SEs
    // r.truncation_corrected · r.detection_probability
    // r.cross_check_relative_gap — vs realized QV − BV
}
Read the full guide →
Truncation-corrected intensity · Merton fixture · P-measure anchor

Performance as a first-class feature

Speed is designed in, not bolted on. These are single-core, release-build wall-clock measurements on an Apple M1 Pro — per-tick streaming latency, batch transforms, and a head-to-head against PyWavelets — with the hot paths held to a Criterion budget in CI so a regression fails the build before it ships.

~67 ns
~15M ticks / sec · 1 core
Streaming MODWT, per tick

StreamingMODWT at Db4, 4 levels — one new coefficient set per update(sample).

42 ns
p50 · ~24M ticks / sec
Streaming denoise, per tick

StreamingDenoiser at Db4, 4 levels — one clean sample per tick, zero steady-state allocation.

100–600 ns
10–100 Morlet scales
Streaming CWT column, per tick

StreamingCWT via cascaded biquads — a fresh (power, phase) column on every sample.

15–20×
within 1.1–2× of fCWT C++
FFT-accelerated CWT

cwt_fft over the full series, versus the direct-convolution reference path.

~10–15%
over cold sswt() recompute
Streaming SSWT, plan-amortized

Cached CWT spectra and reused buffers — bit-identical to the batch synchrosqueezed transform.

0 alloc
per tick · steady state
Streaming steady state

Bounded ring buffer, in-place updates — no per-tick heap traffic after warm-up.

Head to head vs PyWavelets

Apple M1 Pro · single-threaded · release build · PyWavelets 1.8.0 from Python · N = 4096

PathFerroWavePyWavelets (Python)Result
Multilevel DWT · Db4 · 6 levels~20 µs~41 µs~2× faster
Undecimated MODWT · Db4 · 6 levels~61 µs~197 µs (SWT)~3.2× faster
CWT · Morlet · 32 scales~340 µs~5.6 ms~16× faster

Medians of repeated runs on an Apple M1 Pro, single-threaded, release build — PyWavelets 1.8.0 driven from Python over identical N = 4096inputs. The MODWT row compares against PyWavelets' undecimated SWT, its closest peer transform. Beyond speed, PyWavelets and scipy are also FerroWave's numerical-equivalence references: every DWT, MODWT, SWT, WPT, and CWT coefficient is pinned to them at 1e-10, so a Python feature pipeline ports to Rust with bit-for-bit the same numbers. Against the fCWT C++ library, the FFT-accelerated CWT lands within 1.1–2×.

Built from first principles

FerroWave is not a wrapper over a C or Python library. Every transform starts from the source research, is implemented in-tree in pure Rust, is validated against a canonical reference and an independent oracle, and has its core transform laws machine-checked in Lean before it ships.

01

Start from the research

Each transform comes from its source paper, not a second-hand transcription — Daubechies–Lu–Wu (2011) for the synchrosqueezed transform, Huang (1998) for EMD, Dragomiretskiy–Zosso (2014) for VMD, Gilles (2013) for EWT, Torrence–Compo (1998) for the CWT scale↔period map, and Percival & Walden (2000) for MODWT.

02

Implement in-tree, pure Rust

The numerics live in the crate — no FFI hop, no Python runtime, no GIL. SIMD inner loops dispatch through pulp for runtime ISA selection and fall back to a scalar reference path under --no-default-features, so the same code compiles to one statically-linked binary.

03

Validate against canon and oracles

Every DWT, MODWT, SWT, WPT, and CWT coefficient is pinned to PyWavelets and scipy at 1e-10, with reference vectors checked in as exact values — no runtime dependency on Python, R, or MATLAB in CI. Property suites guard round-trip and energy-conservation invariants; mutation testing runs on the core wavelet path.

04

Reproducible by construction

The same inputs always produce the same numbers. Each streaming transform is bit-identical to its batch call at window-fill and on every recompute tick, and every SIMD kernel is bit-tracked against the scalar twin it replaces.

05

Proven, not just tested

The transform mathematics is machine-checked in Lean — 125 theorems with no sorry, kernel-verified against a minimal axiom base: perfect reconstruction IDWT(DWT(x)) = xat every level, Parseval / energy preservation, filter orthonormality, the MODWT, and EMD reconstruction. These laws can't drift from the code.

06

Verified down to the floating point

Beyond the reals: because each transform is an exact-coefficient convolution, its DWT step carries a tight rounding bound (~1 ulp, Gappa), and the proved identities are checked holding in the shipping f64 code at machine epsilon. The FFT-based CWT's accumulation is measured at the textbook ~log₂N·ε.

Past the transform

Three analysis primitives sit on top of the transform layer:denoise (Donoho-Johnstone thresholding over DWT/MODWT/SWT), wavelet coherence(Liu-Torrence-Grinsted cross-spectrum), and multifractal (wavelet-leader Hurst and singularity spectrum).

denoise
soft · hard · garrote
Threshold-based shrinkage
Universal, SURE, and BayesShrink rules over DWT, MODWT, and SWT bases — for noise removal that respects discontinuities.
cohere
wavelet cross-spectrum
Time-frequency coherence
Localized correlation between two signals across scale and time, with the wavelet cross-spectrum and coherence surfaces.
H(q)
wavelet leaders
Multifractal spectrum
Hurst exponents and the multifractal spectrum via wavelet leader analysis — for scaling behavior and intermittency.

Stable signal contracts on the engine

ferro-wave-finance is the product-contract layer built on the core wavelet engine — versioned, serializable signals for jump detection, parametric jump priors, market regime, and anomaly scoring, each grounded in a named method from the literature. The core crate stays wavelet-focused; the finance APIs live here.

JD

Jump & event detection

The canonical Xue–Gençay–Fagan wavelet jump test — bipower-variation noise scale, a closed-form per-wavelet null, continuous p-values and a neg_log_p anomaly score, with optional intraday de-seasonalization. Detected jumps are classified along reflexivity, mean-reversion, and trend-alignment axes (Aubrun et al., “Riding Wavelets”).

λμσ

Parametric jump-diffusion prior

jump_diffusion_prior_signal turns the same wavelet detection substrate into a physical-measure Merton triple — annualized intensity λ, log-jump mean and dispersion (μ_J, σ_J) — with estimator-derived uncertainty, not a point guess. The payload carries the realized QV / BV legs so a consumer can cross-check the prior against model-free jump variance. It is the P-measure anchor an option-pricing calibrator tilts into Q.

1/p

Truncation-corrected intensity

A threshold detector only sees jumps above its own cutoff, so a naive λ is biased low. FerroWave undoes its own conditioning: the detected sizes are refitted as an outer-truncated normal by EM and λ is inflated by the fitted 1/p. On the Merton fixture that reads λ = 4.06 against a realized 4.5, versus 3.2 detection-conditional. It fails safe rather than fabricating — on any EM trouble or a p under the floor, the uncorrected triple ships with a diagnostic saying so.

MP

Market posture & regime

A composite MarketPosture score — event-gated (severity- and recency-aware) and weight-normalized to a symmetric sizing range over volatility, jump, trend, and spectral drivers — plus a TrendRegime classifier that separates a down-trending Bear from a directionless Volatile tape.

Σ

Spectral fingerprint & anomaly

A CWT spectral fingerprint / global wavelet spectrum with a regime-entropy diagnostic, WPT band-energy spectral entropy as a regime-certainty indicator, and a threshold-free multi-scale ECOD wavelet anomaly score with transparent per-band attribution.

{}

Versioned contracts & dispatch

A self-describing FinanceApiConfig with a requested-signals dispatcher and per-signal profile derivation, daily-EOD signals and EOD Wave Pack DTOs, and explicitly versioned API identifiers so a stored payload is never silently read against an incompatible contract.

Mathematical correctness as a gate

Three things sit on the release gate: numerical equality against canonical references (PyWavelets, scipy, Donoho-Johnstone 1994, Torrence-Compo 1998), proptest property suites against round-trip and energy-conservation invariants, and cargo mutants mutation testing on the core wavelet path. Stable transform laws also move into formal proof gates where the numerical contract is precise enough to specify.

Pinned to canonical references

Every transform is audited against PyWavelets, scipy, Donoho–Johnstone 1994, Percival & Walden, and Torrence–Compo 1998 — pinned at 1e-10. The icwt round-trip correlation runs at ≥0.987 across every wavelet × signal pair.

FM

Formal Proof Gates

125 machine-checked Lean theorems over the transform mathematics — kernel-verified with no sorry on the three standard axioms: perfect reconstruction, Parseval / energy, and orthonormality — plus tight Gappa rounding bounds on the transform step and an f64 conformance harness that holds the proved laws at machine epsilon. The claim is scoped to those laws, not every downstream model.

PBT

Property + Mutation Testing

1,900+ tests across unit, integration, property, and reference-fixture suites, with mutation testing on the core wavelet path. Mutation tests catch the regressions property tests miss.

3

Modular Crates

ferro-wave stays wavelet-focused. Finance product contracts live in ferro-wave-finance; runnable workflows live in ferro-wave-examples. Promotion between layers is explicit.

No surprises in the API contract

Wavelet bugs hide in boundary handling and normalization. FerroWave names those choices on the surface so a reader of the call site can reproduce the math without reading the source.

∂Ω

Boundary modes are explicit

One BoundaryMode enum — Periodic, Symmetric, ZeroPad, Reflect — passed at the call site. There is no implicit edge extension hidden inside a transform.

N

PyWavelets-canonical lengths

DWT output lengths match PyWavelets exactly across odd and even N. MODWT, SWT, and SST are shift-invariant — no downsampling — so every level keeps the input length.

‖·‖²

Energy identities hold

Each transform documents its normalization and the Parseval identity it satisfies — MODWT ‖V‖²+‖W‖²=‖x‖², the SWT two-channel frame ‖A‖²+‖D‖²=2‖x‖² — so band variances attribute cleanly.

{}

Configs are non-exhaustive

Every *Config is a #[non_exhaustive] struct with a Default impl and with_* builders, so new knobs land without breaking call sites. One Wavelet trait, one Signal<T>, one Result<_> across the library.

Signal in, typed Result out

Free functions per transform, builder configs, typed result structs

Every transform follows the same shape: Signal<f64> in, a transform-specific Result struct out, wrapped in ferro_wave::Result<_>. Batch entry points are free functions — dwt, modwt, swt, wpt, cwt, cwt_fft, sswt, emd, vmd, ewt, memd, hilbert_spectrum — paired with their inverses where the transform is invertible.

Configuration uses *Config structs with Default impls and with_* builders, marked #[non_exhaustive] so adding fields is non-breaking. Wavelets implement a single Wavelet trait; boundary handling is one BoundaryMode enum across the library.

// Decompose a 1-D signal into 4 levels of MODWT detail +
// scaling coefficients with the Db4 wavelet, then reconstruct
// the original. MODWT is shift-invariant — no downsampling — so
// every level keeps the input length and the round-trip is
// numerically tight.

use ferro_wave::{Signal, transform::{modwt_multilevel, imodwt_multilevel}};
use ferro_wave::wavelets::{Daubechies, DaubechiesType};

fn roundtrip(samples: Vec<f64>) -> ferro_wave::Result<Signal<f64>> {
    let signal   = Signal::new(samples);
    let wavelet  = Daubechies::new(DaubechiesType::Db4);

    let coeffs   = modwt_multilevel(&signal, &wavelet, 4)?;
    // coeffs.wavelet_coeffs[level]  — details per scale
    // coeffs.scaling_coeffs         — coarsest approximation

    let restored = imodwt_multilevel(&coeffs, &wavelet)?;
    Ok(restored)   // max |Δ| ≤ 1e-10 across the wavelet × length × levels sweep
}
Signal<T> in, Result outForward + inverse pairedBuilder configsOne Wavelet trait

Production-tier wavelet code

Most wavelet tooling targets research and teaching workflows. ferro-wave targets the deployment tier — wavelet pipelines that compile into a low-latency production system instead of running on top of a research runtime.

  • Pure Rust — single statically-linked binary, no runtime, no GIL, no FFI hop.
  • Streaming latency at nanosecond scale: ~67 ns per tick for StreamingMODWT (Db4, 4 levels), 42 ns p50 for StreamingDenoiser, 100–600 ns for StreamingCWT across 10–100 scales.
  • One Wavelet trait, one Signal<T> type, one BoundaryMode enum across classical and adaptive families — same shape from dwt to sswt to memd.
  • Numerical equivalence pinned at 1e-10 against PyWavelets and scipy; ICWT round-trip correlation ≥ 0.987 across every wavelet × signal pair.

Talk to us

Reach out for design-partner support or integration guidance. The transform core is formally verified — the math is proven, not just tested.

hello@morphiqlabs.com

Tell us about your use case

  • Your data shape — tick, OHLCV, multi-asset, asset class
  • Performance budget per update or per batch
  • Wavelet family and analysis primitives needed
  • Integration timeline and existing infrastructure