From 15fccccdb23b7c636db01a52fb402c75f73e43bb Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 18 Feb 2026 18:11:28 +0100 Subject: [PATCH 1/5] test: add simple benchmark --- benchmark/reimFFT.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 benchmark/reimFFT.ts diff --git a/benchmark/reimFFT.ts b/benchmark/reimFFT.ts new file mode 100644 index 00000000..ee4d11d9 --- /dev/null +++ b/benchmark/reimFFT.ts @@ -0,0 +1,28 @@ +import { reimFFT } from '../src/reim/reimFFT.ts'; + +const size = 2 ** 16; +const re = new Float64Array(size); +const im = new Float64Array(size); +for (let i = 0; i < size; i++) { + re[i] = Math.random(); + im[i] = Math.random(); +} +// Warmup +reimFFT({ re, im }); + +// Benchmark: repeat until ~5s +const targetMs = 5000; +let iterations = 0; + +console.time('reimFFT'); +const start = performance.now(); +while (performance.now() - start < targetMs) { + reimFFT({ re, im }); + iterations++; +} +console.timeEnd('reimFFT'); + +console.log(`${iterations} iterations, ${size} points each`); +console.log( + `${((performance.now() - start) / iterations).toFixed(2)} ms per iteration`, +); From 1138c4881b2800a1bd837303d879b3336f220e41 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 18 Feb 2026 18:11:46 +0100 Subject: [PATCH 2/5] chore: ignore CPU. files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7dea02f3..967aa5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ dist .DS_Store .eslintcache + +CPU.* \ No newline at end of file From 767a249957c1e4f20f8569c4ee09297a2aba1bd2 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 18 Feb 2026 18:13:53 +0100 Subject: [PATCH 3/5] feat: allow inplace reimFFT closes: https://github.com/mljs/spectra-processing/issues/342 --- src/reim/__tests__/reimFFT.test.ts | 13 ++++++++++ src/reim/reimFFT.ts | 39 +++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/reim/__tests__/reimFFT.test.ts b/src/reim/__tests__/reimFFT.test.ts index 0af28fd4..346247ad 100644 --- a/src/reim/__tests__/reimFFT.test.ts +++ b/src/reim/__tests__/reimFFT.test.ts @@ -12,4 +12,17 @@ test('reimFFT', () => { }); expect(inverse.re).toStrictEqual(re); + expect(inverse.im).toStrictEqual(im); + // check pointer are different + expect(inverse.re).not.toBe(re); + expect(inverse.im).not.toBe(im); + + const transformed2 = reimFFT({ re, im }, { inPlace: true }); + const inverse2 = reimFFT(transformed2, { inverse: true, inPlace: true }); + + expect(inverse2.re).toStrictEqual(re); + expect(inverse2.im).toStrictEqual(im); + // check pointer are the same + expect(inverse2.re).toBe(re); + expect(inverse2.im).toBe(im); }); diff --git a/src/reim/reimFFT.ts b/src/reim/reimFFT.ts index f1cd6c7b..b82e7594 100644 --- a/src/reim/reimFFT.ts +++ b/src/reim/reimFFT.ts @@ -3,9 +3,13 @@ import FFT from 'fft.js'; import type { DataReIm } from '../types/index.ts'; import { xRotate } from '../x/index.ts'; +let output: Float64Array | undefined; +let complexArray: Float64Array | undefined; export interface ReimFFTOptions { inverse?: boolean; applyZeroShift?: boolean; + inPlace?: boolean; + fft?: FFT; } /** @@ -18,20 +22,24 @@ export function reimFFT( data: DataReIm, options: ReimFFTOptions = {}, ): DataReIm { - const { inverse = false, applyZeroShift = false } = options; - const { re, im } = data; const size = re.length; const csize = size << 1; - let complexArray = new Float64Array(csize); + const { + inPlace = false, + fft = new FFT(size), + inverse = false, + applyZeroShift = false, + } = options; + + if (complexArray?.length !== csize) complexArray = new Float64Array(csize); for (let i = 0; i < csize; i += 2) { complexArray[i] = re[i >>> 1]; complexArray[i + 1] = im[i >>> 1]; } - const fft = new FFT(size); - let output = new Float64Array(csize); + if (output?.length !== csize) output = new Float64Array(csize); if (inverse) { if (applyZeroShift) complexArray = zeroShift(complexArray, true); fft.inverseTransform(output, complexArray); @@ -40,14 +48,21 @@ export function reimFFT( if (applyZeroShift) output = zeroShift(output); } - const newRe = new Float64Array(size); - const newIm = new Float64Array(size); - for (let i = 0; i < csize; i += 2) { - newRe[i >>> 1] = output[i]; - newIm[i >>> 1] = output[i + 1]; + if (inPlace) { + for (let i = 0; i < csize; i += 2) { + re[i >>> 1] = output[i]; + im[i >>> 1] = output[i + 1]; + } + return data as DataReIm; + } else { + const newRe = new Float64Array(size); + const newIm = new Float64Array(size); + for (let i = 0; i < csize; i += 2) { + newRe[i >>> 1] = output[i]; + newIm[i >>> 1] = output[i + 1]; + } + return { re: newRe, im: newIm }; } - - return { re: newRe, im: newIm }; } function zeroShift( From 1f18d3f2e182a7f99f701f25a73ce7c4b75544b6 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 18 Feb 2026 18:22:42 +0100 Subject: [PATCH 4/5] perf: do not recreate FFT is same size --- src/reim/reimFFT.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/reim/reimFFT.ts b/src/reim/reimFFT.ts index b82e7594..76338c7c 100644 --- a/src/reim/reimFFT.ts +++ b/src/reim/reimFFT.ts @@ -5,11 +5,11 @@ import { xRotate } from '../x/index.ts'; let output: Float64Array | undefined; let complexArray: Float64Array | undefined; +let fft: FFT | undefined; export interface ReimFFTOptions { inverse?: boolean; applyZeroShift?: boolean; inPlace?: boolean; - fft?: FFT; } /** @@ -26,12 +26,9 @@ export function reimFFT( const size = re.length; const csize = size << 1; - const { - inPlace = false, - fft = new FFT(size), - inverse = false, - applyZeroShift = false, - } = options; + const { inPlace = false, inverse = false, applyZeroShift = false } = options; + + if (fft?.size !== size) fft = new FFT(size); if (complexArray?.length !== csize) complexArray = new Float64Array(csize); for (let i = 0; i < csize; i += 2) { @@ -74,3 +71,5 @@ function zeroShift( : Math.floor(data.length / 2); return xRotate(data, middle); } + +export { default as FFT } from 'fft.js'; From a968f1ade6e3fe0ca6636e5402949c53ee620da2 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Thu, 19 Feb 2026 06:40:09 +0100 Subject: [PATCH 5/5] chore: fix tests --- eslint.config.js | 5 ++++- src/reim/reimFFT.ts | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 00cc43d2..f7512d55 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,7 @@ import { defineConfig, globalIgnores } from 'eslint/config'; import cheminfo from 'eslint-config-cheminfo-typescript'; -export default defineConfig(globalIgnores(['coverage', 'lib']), cheminfo); +export default defineConfig( + globalIgnores(['coverage', 'lib', 'benchmark']), + cheminfo, +); diff --git a/src/reim/reimFFT.ts b/src/reim/reimFFT.ts index 76338c7c..2bcedbee 100644 --- a/src/reim/reimFFT.ts +++ b/src/reim/reimFFT.ts @@ -71,5 +71,3 @@ function zeroShift( : Math.floor(data.length / 2); return xRotate(data, middle); } - -export { default as FFT } from 'fft.js';