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.
Transforms
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
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
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
AI & ML
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.
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.
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.
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.
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 }
Architecture
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.
Signal
Signal<f64> from a slice or streaming source. Boundary modes — periodic, symmetric, zero-pad, reflect — explicit at the API surface.
Decompose
Run DWT, MODWT, SWT, WPT, or CWT over the signal with a chosen wavelet. SIMD-vectorized inner loops on supported platforms.
Analyze
Read the coefficients: denoise, wavelet coherence, multifractal/Hurst, matching pursuit. The analysis layer never touches the raw signal.
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.
Streaming
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 } }
Use Cases
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.
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
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 }
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();
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+
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
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) }
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.
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 }
Benchmarks
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.
StreamingMODWT at Db4, 4 levels — one new coefficient set per update(sample).
StreamingDenoiser at Db4, 4 levels — one clean sample per tick, zero steady-state allocation.
StreamingCWT via cascaded biquads — a fresh (power, phase) column on every sample.
cwt_fft over the full series, versus the direct-convolution reference path.
Cached CWT spectra and reused buffers — bit-identical to the batch synchrosqueezed transform.
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
| Path | FerroWave | PyWavelets (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×.
Foundations
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.
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.
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.
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.
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.
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.
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·ε.
Analysis
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).
Finance
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.
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.
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.
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.
Code Quality
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.
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.
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.
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.
Conventions
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.
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.
API Surface
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 }
Positioning
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 forStreamingDenoiser, 100–600 ns forStreamingCWTacross 10–100 scales. - One
Wavelettrait, oneSignal<T>type, oneBoundaryModeenum across classical and adaptive families — same shape fromdwttosswttomemd. - Numerical equivalence pinned at
1e-10against 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.comTell 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