Skip to content

Commit 351a745

Browse files
committedAug 16, 2018
Refactor DFT and add common tests for Fourier.
1 parent 13ed506 commit 351a745

File tree

3 files changed

+314
-164
lines changed

3 files changed

+314
-164
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import ComplexNumber from '../../complex-number/ComplexNumber';
2+
3+
export const fourierDirectTestCases = [
4+
{
5+
input: [
6+
{ amplitude: 1 },
7+
],
8+
output: [
9+
{
10+
frequency: 0, amplitude: 1, phase: 0, re: 1, im: 0,
11+
},
12+
],
13+
},
14+
{
15+
input: [
16+
{ amplitude: 1 },
17+
{ amplitude: 0 },
18+
],
19+
output: [
20+
{
21+
frequency: 0, amplitude: 0.5, phase: 0, re: 0.5, im: 0,
22+
},
23+
{
24+
frequency: 1, amplitude: 0.5, phase: 0, re: 0.5, im: 0,
25+
},
26+
],
27+
},
28+
{
29+
input: [
30+
{ amplitude: 2 },
31+
{ amplitude: 0 },
32+
],
33+
output: [
34+
{
35+
frequency: 0, amplitude: 1, phase: 0, re: 1, im: 0,
36+
},
37+
{
38+
frequency: 1, amplitude: 1, phase: 0, re: 1, im: 0,
39+
},
40+
],
41+
},
42+
{
43+
input: [
44+
{ amplitude: 1 },
45+
{ amplitude: 0 },
46+
{ amplitude: 0 },
47+
],
48+
output: [
49+
{
50+
frequency: 0, amplitude: 0.3333, phase: 0, re: 0.3333, im: 0,
51+
},
52+
{
53+
frequency: 1, amplitude: 0.3333, phase: 0, re: 0.3333, im: 0,
54+
},
55+
{
56+
frequency: 2, amplitude: 0.3333, phase: 0, re: 0.3333, im: 0,
57+
},
58+
],
59+
},
60+
{
61+
input: [
62+
{ amplitude: 1 },
63+
{ amplitude: 0 },
64+
{ amplitude: 0 },
65+
{ amplitude: 0 },
66+
],
67+
output: [
68+
{
69+
frequency: 0, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
70+
},
71+
{
72+
frequency: 1, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
73+
},
74+
{
75+
frequency: 2, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
76+
},
77+
{
78+
frequency: 3, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
79+
},
80+
],
81+
},
82+
{
83+
input: [
84+
{ amplitude: 0 },
85+
{ amplitude: 1 },
86+
{ amplitude: 0 },
87+
{ amplitude: 0 },
88+
],
89+
output: [
90+
{
91+
frequency: 0, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
92+
},
93+
{
94+
frequency: 1, amplitude: 0.25, phase: -90, re: 0, im: -0.25,
95+
},
96+
{
97+
frequency: 2, amplitude: 0.25, phase: 180, re: -0.25, im: 0,
98+
},
99+
{
100+
frequency: 3, amplitude: 0.25, phase: 90, re: 0, im: 0.25,
101+
},
102+
],
103+
},
104+
{
105+
input: [
106+
{ amplitude: 0 },
107+
{ amplitude: 0 },
108+
{ amplitude: 1 },
109+
{ amplitude: 0 },
110+
],
111+
output: [
112+
{
113+
frequency: 0, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
114+
},
115+
{
116+
frequency: 1, amplitude: 0.25, phase: 180, re: -0.25, im: 0,
117+
},
118+
{
119+
frequency: 2, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
120+
},
121+
{
122+
frequency: 3, amplitude: 0.25, phase: 180, re: -0.25, im: 0,
123+
},
124+
],
125+
},
126+
{
127+
input: [
128+
{ amplitude: 0 },
129+
{ amplitude: 0 },
130+
{ amplitude: 0 },
131+
{ amplitude: 2 },
132+
],
133+
output: [
134+
{
135+
frequency: 0, amplitude: 0.5, phase: 0, re: 0.5, im: 0,
136+
},
137+
{
138+
frequency: 1, amplitude: 0.5, phase: 90, re: 0, im: 0.5,
139+
},
140+
{
141+
frequency: 2, amplitude: 0.5, phase: 180, re: -0.5, im: 0,
142+
},
143+
{
144+
frequency: 3, amplitude: 0.5, phase: -90, re: 0, im: -0.5,
145+
},
146+
],
147+
},
148+
{
149+
input: [
150+
{ amplitude: 0 },
151+
{ amplitude: 1 },
152+
{ amplitude: 0 },
153+
{ amplitude: 2 },
154+
],
155+
output: [
156+
{
157+
frequency: 0, amplitude: 0.75, phase: 0, re: 0.75, im: 0,
158+
},
159+
{
160+
frequency: 1, amplitude: 0.25, phase: 90, re: 0, im: 0.25,
161+
},
162+
{
163+
frequency: 2, amplitude: 0.75, phase: 180, re: -0.75, im: 0,
164+
},
165+
{
166+
frequency: 3, amplitude: 0.25, phase: -90, re: 0, im: -0.25,
167+
},
168+
],
169+
},
170+
{
171+
input: [
172+
{ amplitude: 4 },
173+
{ amplitude: 1 },
174+
{ amplitude: 0 },
175+
{ amplitude: 2 },
176+
],
177+
output: [
178+
{
179+
frequency: 0, amplitude: 1.75, phase: 0, re: 1.75, im: 0,
180+
},
181+
{
182+
frequency: 1, amplitude: 1.03077, phase: 14.0362, re: 0.99999, im: 0.25,
183+
},
184+
{
185+
frequency: 2, amplitude: 0.25, phase: 0, re: 0.25, im: 0,
186+
},
187+
{
188+
frequency: 3, amplitude: 1.03077, phase: -14.0362, re: 1, im: -0.25,
189+
},
190+
],
191+
},
192+
{
193+
input: [
194+
{ amplitude: 4 },
195+
{ amplitude: 1 },
196+
{ amplitude: -3 },
197+
{ amplitude: 2 },
198+
],
199+
output: [
200+
{
201+
frequency: 0, amplitude: 1, phase: 0, re: 1, im: 0,
202+
},
203+
{
204+
frequency: 1, amplitude: 1.76776, phase: 8.1301, re: 1.75, im: 0.25,
205+
},
206+
{
207+
frequency: 2, amplitude: 0.5, phase: 180, re: -0.5, im: 0,
208+
},
209+
{
210+
frequency: 3, amplitude: 1.76776, phase: -8.13010, re: 1.75, im: -0.24999,
211+
},
212+
],
213+
},
214+
{
215+
input: [
216+
{ amplitude: 1 },
217+
{ amplitude: 2 },
218+
{ amplitude: 3 },
219+
{ amplitude: 4 },
220+
],
221+
output: [
222+
{
223+
frequency: 0, amplitude: 2.5, phase: 0, re: 2.5, im: 0,
224+
},
225+
{
226+
frequency: 1, amplitude: 0.70710, phase: 135, re: -0.5, im: 0.49999,
227+
},
228+
{
229+
frequency: 2, amplitude: 0.5, phase: 180, re: -0.5, im: 0,
230+
},
231+
{
232+
frequency: 3, amplitude: 0.70710, phase: -134.99999, re: -0.49999, im: -0.5,
233+
},
234+
],
235+
},
236+
];
237+
238+
export default class FourierTester {
239+
/**
240+
* @param {function} fourierTransform
241+
*/
242+
static testDirectFourierTransform(fourierTransform) {
243+
fourierDirectTestCases.forEach((testCase) => {
244+
const { input, output: expectedOutput } = testCase;
245+
246+
// Convert input into complex numbers.
247+
const complexInput = input.map(sample => new ComplexNumber({ re: sample.amplitude }));
248+
249+
// Try to split input signal into sequence of pure sinusoids.
250+
const currentOutput = fourierTransform(complexInput);
251+
252+
// Check the signal has been split into proper amount of sub-signals.
253+
expect(currentOutput.length).toBeGreaterThanOrEqual(complexInput.length);
254+
255+
// Now go through all the signals and check their frequency, amplitude and phase.
256+
expectedOutput.forEach((expectedSignal, frequency) => {
257+
// Get template data we want to test against.
258+
const currentSignal = currentOutput[frequency];
259+
const currentPolarSignal = currentSignal.getPolarForm(false);
260+
261+
// Check all signal parameters.
262+
expect(frequency).toBe(expectedSignal.frequency);
263+
expect(currentSignal.re).toBeCloseTo(expectedSignal.re, 4);
264+
expect(currentSignal.im).toBeCloseTo(expectedSignal.im, 4);
265+
expect(currentPolarSignal.phase).toBeCloseTo(expectedSignal.phase, 4);
266+
expect(currentPolarSignal.radius).toBeCloseTo(expectedSignal.amplitude, 4);
267+
});
268+
});
269+
}
270+
}
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,8 @@
11
import discreteFourierTransform from '../discreteFourierTransform';
2-
3-
/**
4-
* Helper class of the output Signal.
5-
*/
6-
class Sgnl {
7-
constructor(frequency, amplitude, phase) {
8-
this.frequency = frequency;
9-
this.amplitude = amplitude;
10-
this.phase = phase;
11-
}
12-
}
2+
import FourierTester from './FourierTester';
133

144
describe('discreteFourierTransform', () => {
155
it('should split signal into frequencies', () => {
16-
const testCases = [
17-
{
18-
inputAmplitudes: [1],
19-
outputSignals: [
20-
new Sgnl(0, 1, 0),
21-
],
22-
},
23-
{
24-
inputAmplitudes: [1, 0],
25-
outputSignals: [
26-
new Sgnl(0, 0.5, 0),
27-
new Sgnl(1, 0.5, 0),
28-
],
29-
},
30-
{
31-
inputAmplitudes: [2, 0],
32-
outputSignals: [
33-
new Sgnl(0, 1, 0),
34-
new Sgnl(1, 1, 0),
35-
],
36-
},
37-
{
38-
inputAmplitudes: [1, 0, 0],
39-
outputSignals: [
40-
new Sgnl(0, 0.33, 0),
41-
new Sgnl(1, 0.33, 0),
42-
new Sgnl(2, 0.33, 0),
43-
],
44-
},
45-
{
46-
inputAmplitudes: [1, 0, 0, 0],
47-
outputSignals: [
48-
new Sgnl(0, 0.25, 0),
49-
new Sgnl(1, 0.25, 0),
50-
new Sgnl(2, 0.25, 0),
51-
new Sgnl(3, 0.25, 0),
52-
],
53-
},
54-
{
55-
inputAmplitudes: [0, 1, 0, 0],
56-
outputSignals: [
57-
new Sgnl(0, 0.25, 0),
58-
new Sgnl(1, 0.25, -90),
59-
new Sgnl(2, 0.25, 180),
60-
new Sgnl(3, 0.25, 90),
61-
],
62-
},
63-
{
64-
inputAmplitudes: [0, 0, 1, 0],
65-
outputSignals: [
66-
new Sgnl(0, 0.25, 0),
67-
new Sgnl(1, 0.25, 180),
68-
new Sgnl(2, 0.25, 0),
69-
new Sgnl(3, 0.25, 180),
70-
],
71-
},
72-
{
73-
inputAmplitudes: [0, 0, 0, 2],
74-
outputSignals: [
75-
new Sgnl(0, 0.5, 0),
76-
new Sgnl(1, 0.5, 90),
77-
new Sgnl(2, 0.5, 180),
78-
new Sgnl(3, 0.5, -90),
79-
],
80-
},
81-
{
82-
inputAmplitudes: [0, 1, 0, 2],
83-
outputSignals: [
84-
new Sgnl(0, 0.75, 0),
85-
new Sgnl(1, 0.25, 90),
86-
new Sgnl(2, 0.75, 180),
87-
new Sgnl(3, 0.25, -90),
88-
],
89-
},
90-
{
91-
inputAmplitudes: [4, 1, 0, 2],
92-
outputSignals: [
93-
new Sgnl(0, 1.75, 0),
94-
new Sgnl(1, 1.03, 14),
95-
new Sgnl(2, 0.25, 0),
96-
new Sgnl(3, 1.03, -14),
97-
],
98-
},
99-
{
100-
inputAmplitudes: [4, 1, -3, 2],
101-
outputSignals: [
102-
new Sgnl(0, 1, 0),
103-
new Sgnl(1, 1.77, 8),
104-
new Sgnl(2, 0.5, 180),
105-
new Sgnl(3, 1.77, -8),
106-
],
107-
},
108-
{
109-
inputAmplitudes: [1, 2, 3, 4],
110-
outputSignals: [
111-
new Sgnl(0, 2.5, 0),
112-
new Sgnl(1, 0.71, 135),
113-
new Sgnl(2, 0.5, 180),
114-
new Sgnl(3, 0.71, -135),
115-
],
116-
},
117-
];
118-
119-
testCases.forEach((testCase) => {
120-
const { inputAmplitudes, outputSignals } = testCase;
121-
122-
// Try to split input signal into sequence of pure sinusoids.
123-
const signals = discreteFourierTransform(inputAmplitudes);
124-
125-
// Check the signal has been split into proper amount of sub-signals.
126-
expect(signals.length).toBe(outputSignals.length);
127-
128-
// Now go through all the signals and check their frequency, amplitude and phase.
129-
signals.forEach((signal, frequency) => {
130-
// Get polar form of calculated sub-signal since it is more convenient to analyze.
131-
const signalPolarForm = signal.getPolarForm(false);
132-
133-
// Get template data we want to test against.
134-
const signalTemplate = outputSignals[frequency];
135-
136-
// Check all signal parameters.
137-
expect(frequency).toBe(signalTemplate.frequency);
138-
expect(signalPolarForm.radius).toBeCloseTo(signalTemplate.amplitude, 2);
139-
expect(signalPolarForm.phase).toBeCloseTo(signalTemplate.phase, 0);
140-
});
141-
});
6+
FourierTester.testDirectFourierTransform(discreteFourierTransform);
1427
});
1438
});
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,70 @@
11
import ComplexNumber from '../complex-number/ComplexNumber';
22

3+
const CLOSE_TO_ZERO_THRESHOLD = 1e-10;
4+
35
/**
4-
* @param {number[]} inputSignalAmplitudes - Input signal amplitudes over time (i.e. [1, 0, 4]).
6+
* Discrete Fourier Transform.
7+
*
8+
* Time complexity: O(N^2)
9+
*
10+
* @param {ComplexNumber[]} complexInputAmplitudes - Input signal amplitudes over time (complex
11+
* numbers with real parts only).
12+
*
513
* @return {ComplexNumber[]} - Array of complex number. Each of the number represents the frequency
614
* or signal. All signals together will form input signal over discrete time periods. Each signal's
715
* complex number has radius (amplitude) and phase (angle) in polar form that describes the signal.
816
*
917
* @see https://gist.github.com/anonymous/129d477ddb1c8025c9ac
1018
* @see https://betterexplained.com/articles/an-interactive-guide-to-the-fourier-transform/
1119
*/
12-
export default function discreteFourierTransform(inputSignalAmplitudes) {
13-
const N = inputSignalAmplitudes.length;
14-
const outputFrequencies = [];
20+
export default function dft(complexInputAmplitudes) {
21+
// Convert complex amplitudes into real ones.
22+
const inputAmplitudes = complexInputAmplitudes.map(complexAmplitude => complexAmplitude.re);
23+
24+
const N = inputAmplitudes.length;
25+
const signals = [];
1526

16-
// For every frequency discrete...
17-
for (let frequencyValue = 0; frequencyValue < N; frequencyValue += 1) {
18-
let signal = new ComplexNumber();
27+
// Go through every discrete frequency.
28+
for (let frequency = 0; frequency < N; frequency += 1) {
29+
// Compound signal at current frequency that will ultimately
30+
// take part in forming input amplitudes.
31+
let frequencySignal = new ComplexNumber();
1932

20-
// For every discrete point in time...
21-
for (let t = 0; t < N; t += 1) {
22-
// Spin the signal _backwards_ at each frequency (as radians/s, not Hertz)
23-
const rate = -1 * (2 * Math.PI) * frequencyValue;
33+
// Go through every discrete point in time.
34+
for (let timer = 0; timer < N; timer += 1) {
35+
const currentAmplitude = inputAmplitudes[timer];
2436

25-
// How far around the circle have we gone at time=t?
26-
const time = t / N;
27-
const distance = rate * time;
37+
// Calculate rotation angle.
38+
const rotationAngle = -1 * (2 * Math.PI) * frequency * (timer / N);
2839

29-
// Data-point * e^(-i*2*pi*f) is complex, store each part.
40+
// Remember that e^ix = cos(x) + i * sin(x);
3041
const dataPointContribution = new ComplexNumber({
31-
re: inputSignalAmplitudes[t] * Math.cos(distance),
32-
im: inputSignalAmplitudes[t] * Math.sin(distance),
33-
});
42+
re: Math.cos(rotationAngle),
43+
im: Math.sin(rotationAngle),
44+
}).multiply(currentAmplitude);
3445

3546
// Add this data point's contribution.
36-
signal = signal.add(dataPointContribution);
47+
frequencySignal = frequencySignal.add(dataPointContribution);
3748
}
3849

3950
// Close to zero? You're zero.
40-
if (Math.abs(signal.re) < 1e-10) {
41-
signal.re = 0;
51+
if (Math.abs(frequencySignal.re) < CLOSE_TO_ZERO_THRESHOLD) {
52+
frequencySignal.re = 0;
4253
}
4354

44-
if (Math.abs(signal.im) < 1e-10) {
45-
signal.im = 0;
55+
if (Math.abs(frequencySignal.im) < CLOSE_TO_ZERO_THRESHOLD) {
56+
frequencySignal.im = 0;
4657
}
4758

48-
// Average contribution at this frequency
49-
signal = signal.divide(N);
59+
// Average contribution at this frequency.
60+
// The 1/N factor is usually moved to the reverse transform (going from frequencies
61+
// back to time). This is allowed, though it would be nice to have 1/N in the forward
62+
// transform since it gives the actual sizes for the time spikes.
63+
frequencySignal = frequencySignal.divide(N);
5064

51-
outputFrequencies[frequencyValue] = signal;
65+
// Add current frequency signal to the list of compound signals.
66+
signals[frequency] = frequencySignal;
5267
}
5368

54-
return outputFrequencies;
69+
return signals;
5570
}

0 commit comments

Comments
 (0)
Please sign in to comment.