Skip to content

Commit 9cda6b5

Browse files
authored
feat: add reimArrayFFT
feat: add reimArrayFFT feat: add inPlace for reimArrayFFT and reimFFT closes: #342
1 parent b829991 commit 9cda6b5

File tree

11 files changed

+415
-19
lines changed

11 files changed

+415
-19
lines changed

benchmark/reimFFT.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* eslint-disable no-console */
2+
import { reimFFT } from '../src/reim/reimFFT.ts';
3+
import { reimArrayFFT } from '../src/reimArray/reimArrayFFT.ts';
4+
5+
const size = 2 ** 16;
6+
const count = 10; // number of spectra in the array benchmark
7+
8+
// Build input data
9+
const spectra = Array.from({ length: count }, () => {
10+
const re = new Float64Array(size);
11+
const im = new Float64Array(size);
12+
for (let i = 0; i < size; i++) {
13+
re[i] = Math.random();
14+
im[i] = Math.random();
15+
}
16+
return { re, im };
17+
});
18+
19+
// Warmup
20+
for (const s of spectra) reimFFT(s);
21+
for (const s of spectra) reimFFT(s, { inPlace: true });
22+
reimArrayFFT(spectra);
23+
reimArrayFFT(spectra, { inPlace: true });
24+
25+
const targetMs = 5000;
26+
27+
// --- reimFFT (loop over each spectrum individually) ---
28+
{
29+
let iterations = 0;
30+
const start = performance.now();
31+
console.time('reimFFT (loop)');
32+
while (performance.now() - start < targetMs) {
33+
for (const s of spectra) reimFFT(s);
34+
iterations++;
35+
}
36+
const elapsed = performance.now() - start;
37+
console.timeEnd('reimFFT (loop)');
38+
console.log(
39+
` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`,
40+
);
41+
console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`);
42+
}
43+
44+
console.log('');
45+
46+
// --- reimFFT inPlace (loop over each spectrum individually) ---
47+
{
48+
let iterations = 0;
49+
const start = performance.now();
50+
console.time('reimFFT inPlace (loop)');
51+
while (performance.now() - start < targetMs) {
52+
for (const s of spectra) reimFFT(s, { inPlace: true });
53+
iterations++;
54+
}
55+
const elapsed = performance.now() - start;
56+
console.timeEnd('reimFFT inPlace (loop)');
57+
console.log(
58+
` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`,
59+
);
60+
console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`);
61+
}
62+
63+
console.log('');
64+
65+
// --- reimArrayFFT (single call for the whole array) ---
66+
{
67+
let iterations = 0;
68+
const start = performance.now();
69+
console.time('reimArrayFFT');
70+
while (performance.now() - start < targetMs) {
71+
reimArrayFFT(spectra);
72+
iterations++;
73+
}
74+
const elapsed = performance.now() - start;
75+
console.timeEnd('reimArrayFFT');
76+
console.log(
77+
` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`,
78+
);
79+
console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`);
80+
}
81+
82+
console.log('');
83+
84+
// --- reimArrayFFT inPlace (single call for the whole array) ---
85+
{
86+
let iterations = 0;
87+
const start = performance.now();
88+
console.time('reimArrayFFT inPlace');
89+
while (performance.now() - start < targetMs) {
90+
reimArrayFFT(spectra, { inPlace: true });
91+
iterations++;
92+
}
93+
const elapsed = performance.now() - start;
94+
console.timeEnd('reimArrayFFT inPlace');
95+
console.log(
96+
` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`,
97+
);
98+
console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`);
99+
}

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,19 @@
4747
"ml-xsadd": "^3.0.1"
4848
},
4949
"devDependencies": {
50-
"@types/node": "^25.0.3",
51-
"@vitest/coverage-v8": "^4.0.16",
50+
"@types/node": "^25.3.0",
51+
"@vitest/coverage-v8": "^4.0.18",
5252
"@zakodium/tsconfig": "^1.0.2",
5353
"cheminfo-build": "^1.3.2",
5454
"eslint": "^9.39.2",
55-
"eslint-config-cheminfo-typescript": "^21.0.1",
55+
"eslint-config-cheminfo-typescript": "^21.1.0",
5656
"jest-matcher-deep-close-to": "^3.0.2",
5757
"ml-spectra-fitting": "^5.0.1",
58-
"prettier": "^3.7.4",
59-
"rimraf": "^6.1.2",
58+
"prettier": "^3.8.1",
59+
"rimraf": "^6.1.3",
6060
"spectrum-generator": "^8.1.1",
6161
"typescript": "^5.9.3",
62-
"vitest": "^4.0.16"
62+
"vitest": "^4.0.18"
6363
},
6464
"repository": {
6565
"type": "git",

src/__tests__/__snapshots__/index.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ exports[`existence of exported functions 1`] = `
77
"reimFFT",
88
"reimPhaseCorrection",
99
"reimZeroFilling",
10+
"reimArrayFFT",
1011
"getOutputArray",
1112
"xAbsolute",
1213
"xAbsoluteMedian",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './reim/index.ts';
2+
export * from './reimArray/index.ts';
23

34
export * from './x/index.ts';
45

src/reim/__tests__/reimFFT.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,29 @@ test('reimFFT', () => {
1313

1414
expect(inverse.re).toStrictEqual(re);
1515
});
16+
17+
test('reimFFT inPlace: returns same object and mutates input arrays', () => {
18+
const re = Float64Array.from([0, 3, 6, 5]);
19+
const im = Float64Array.from([0, 4, 8, 3]);
20+
const data = { re, im };
21+
22+
const result = reimFFT(data, { inPlace: true });
23+
24+
expect(result).toBe(data);
25+
expect(result.re).toBe(re);
26+
expect(result.im).toBe(im);
27+
});
28+
29+
test('reimFFT inPlace: round-trip restores original values', () => {
30+
const re = Float64Array.from([0, 3, 6, 5]);
31+
const im = Float64Array.from([0, 4, 8, 3]);
32+
const reOrig = Float64Array.from(re);
33+
const imOrig = Float64Array.from(im);
34+
const data = { re, im };
35+
36+
reimFFT(data, { inPlace: true, applyZeroShift: true });
37+
reimFFT(data, { inPlace: true, inverse: true, applyZeroShift: true });
38+
39+
expect(data.re).toStrictEqual(reOrig);
40+
expect(data.im).toStrictEqual(imOrig);
41+
});

src/reim/reimFFT.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import FFT from 'fft.js';
22

33
import type { DataReIm } from '../types/index.ts';
4-
import { xRotate } from '../x/index.ts';
4+
5+
import { zeroShift } from './zeroShift.ts';
56

67
export interface ReimFFTOptions {
78
inverse?: boolean;
89
applyZeroShift?: boolean;
10+
/**
11+
* Write the result back into the input arrays instead of allocating new ones.
12+
* @default false
13+
*/
14+
inPlace?: boolean;
915
}
1016

1117
/**
@@ -18,7 +24,7 @@ export function reimFFT(
1824
data: DataReIm,
1925
options: ReimFFTOptions = {},
2026
): DataReIm<Float64Array> {
21-
const { inverse = false, applyZeroShift = false } = options;
27+
const { inverse = false, applyZeroShift = false, inPlace = false } = options;
2228

2329
const { re, im } = data;
2430
const size = re.length;
@@ -40,6 +46,14 @@ export function reimFFT(
4046
if (applyZeroShift) output = zeroShift(output);
4147
}
4248

49+
if (inPlace) {
50+
for (let i = 0; i < csize; i += 2) {
51+
re[i >>> 1] = output[i];
52+
im[i >>> 1] = output[i + 1];
53+
}
54+
return data as DataReIm<Float64Array>;
55+
}
56+
4357
const newRe = new Float64Array(size);
4458
const newIm = new Float64Array(size);
4559
for (let i = 0; i < csize; i += 2) {
@@ -49,13 +63,3 @@ export function reimFFT(
4963

5064
return { re: newRe, im: newIm };
5165
}
52-
53-
function zeroShift(
54-
data: Float64Array,
55-
inverse?: boolean,
56-
): Float64Array<ArrayBuffer> {
57-
const middle = inverse
58-
? Math.ceil(data.length / 2)
59-
: Math.floor(data.length / 2);
60-
return xRotate(data, middle);
61-
}

src/reim/zeroShift.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { xRotate } from '../x/index.ts';
2+
3+
export function zeroShift(
4+
data: Float64Array,
5+
inverse?: boolean,
6+
): Float64Array<ArrayBuffer> {
7+
const middle = inverse
8+
? Math.ceil(data.length / 2)
9+
: Math.floor(data.length / 2);
10+
11+
return xRotate(data, middle);
12+
}

0 commit comments

Comments
 (0)