I averaged the complex FFT of a number of acquisitions.
You mean averaging fft(CH1) and fft(CH2) separately?
FFT is a linear operation (multiplication of the time domain signal vector of length N with a huge complex NxN matrix), and average is basically just a sum and division by a constant, so the distributive law applies. Therefore average(fft(CH1)) is equivalent to fft(average(CH1)). So what you get is the fft of the average of the CH1 traces. [ Btw, this also offers a cheaper alternative to calculate this average: Average the time domain traces and calculate the FFT only once. ]
Note that this kind of averaging will only give you the desired result if the traces to be averaged are in phase
for each frequency contained in the signal. In the end, you still rely on the trigger to do this alignment. But this does not work in general. Of course, it is supposed to work if the same periodic waveform is captured multiple times. But it will not do what you want it to do if you average two traces of, say, a 25% duty cycle square wave, where one trace captures a 10 Hz tone and the second trace captures a 100 Hz tone, both triggered on the rising edge. Then the 10th harmonic of the 10 Hz tone in the first trace is
not in phase with the fundamental of the 100 Hz tone in the 2nd trace (-pi/4 vs. -pi/2). Therefore, they do not qualify for this kind of averaging.
What I actually had in mind instead is average(fft(CH2)/fft(CH1)), i.e. averaging the complex gains of multiple acquisitions. The expected value for the phase angles of this quotient is the phase of the DUT's transfer function, which is invariant with respect to the acquisition phase. Therefore, this average does not depend on the trigger, but relies only on the coherence between CH1 and CH2, which is achieved by sampling CH1 and CH2 simultaneously.
I apply a Hanning window over the data at the cost of some SNR. That's more needed for plotting single channel FFT. The same code is reused for the Bode plot. I now realize this is perhaps not needed. The frequency bleeding will be the same for both channels for which I plot the difference.
There are two main considerations:
1)
Scalloping loss: You are correct, it cancels out in the complex gain fft(CH2) / fft(CH1), but of course it also reduces the SNR because a lower signal level is detected by the FFT bin. OTOH, for displaying the spectrum of a periodic signal you usually want to avoid scalloping loss by using a flattop window in order to show the amplitudes of the harmonics correctly, even for arbitrary signal frequencies.
2)
Bleeding between harmonics does not cancel out, and therefore also matters for the complex gain. Especially for a narrowband DUT, you likely want a window function with a high selectivity and high stopband attenuation (maybe even 100+ dB), like e.g. Blackman-Harris, Blackman-Nuttall, or Kaiser with an appropriate beta.
EDIT:
Plotting fft(CH1) and fft(CH2) is, of course, only for your diagnosis.
It's not part of the actual Bode plot.
EDIT:
For better understanding, here is pseudo code for what I had in mind:
// initialize
for f in 0...FFT_SIZE-1 {
gain[f] = 0 + 0i; // complex
weight_sum[f] = 0;
}
for each acquisition {
Vin = fft(CH1);
Vout = fft(CH2);
for f in 0...FFT_SIZE-1 {
weight = abs(Vin[f]); // magnitude of reference signal at frequency f
// skip frequencies where the drive level is too low
if (weight > threshold) {
gain[f] += Vout[f] / Vin[f] * weight;
weight_sum[f] += weight;
}
}
}
for f in 0...FFT_SIZE-1 {
if (weight_sum[f] > 0)
gain[f] /= weight_sum[f];
}
discard/ignore all points gain[f] where weight_sum[f] == 0
plot magnitude and phase of the remaining points.
[code]