Bit-exact, cross-platform polyphase rational resampling (upfirdn) for signal & on-device-ML preprocessing.
Resample a 1-D signal by a rational factor L/M — upsample by L, FIR-filter, downsample by M — with output that is identical down to float64 on every runtime (TypeScript, Python, Kotlin, Swift) given the same kernel, and that matches scipy.signal.upfirdn to float64 precision.
On-device ML breaks silently when preprocessing differs between platforms. A model trained on signals resampled in Python (say 500 Hz → 300 Hz) but run on a watch / phone / desktop that resamples even slightly differently sees a shifted input distribution — and accuracy quietly drops. Floating-point drift between a NumPy/SciPy training pipeline and a hand-written mobile resampler is a classic, painful source of train/serve skew.
The fix: implement the resampler once, with a single explicit accumulation order, so the same (kernel, x, up, down) yields the same samples on every platform and on the training pipeline — no drift, no skew. Watch (Kotlin), iOS (Swift), and desktop/browser (TypeScript) all agree with NumPy/SciPy.
import { upfirdn, resamplePoly } from "./src/upfirdn";
// Resample 500 Hz -> 300 Hz (gcd(500,300)=100 -> up=3, down=5)
const out = resamplePoly(signal, 3, 5, firKernel); // firKernel = your shared FIR tapsupfirdn(h, x, up, down) has the same semantics as scipy.signal.upfirdn.
- Cross-platform: one fixed accumulation order → bit-identical float64 output on any IEEE-754 runtime.
- Vs SciPy:
scripts/golden.pyrunsscipy.signal.upfirdnon random inputs and writes golden vectors;npm run verifychecks max abs error < 1e-12.
python scripts/golden.py # needs numpy + scipy
npm run verify # compares TS output against the golden vectorsNote on "bit-exact": within this implementation the output is exact and identical across platforms (fixed op order). Against SciPy the agreement is to ~1e-12 — SciPy's internal polyphase accumulation order differs; the algorithm and result are otherwise the same. Share the FIR kernel across platforms (as JSON) and every port produces the same numbers.
So the resampler is fully deterministic: ship one kernel (e.g. a Kaiser-windowed sinc) as data across all platforms instead of re-designing the filter per language. Same data + same algorithm = same output everywhere.
MIT