4

I'm currently working on a spare-time project to perform automatic modulation classification (AMC) on radio signals (more precisely, I'm interested in L-band satellite channels), using SDR. I would like it to be able to discover channels in the power spectrum, along with their frequencies and bandwidths, so I can direct the application of AMC on the output of this step.

My first (naive) approach was rather simple: after every N samples (say 1024) apply a window function, perform the FFT on the last N, compute an estimation of the power spectrum and apply some exponential smoothing to reduce the noise. Then, in the smoothed power spectrum, find the maximum and minimum signal levels, calculate some threshold value based on a weighted mean of both levels and use this threshold to determine which frequency bins belong to a channel.

This works well in my unit tests (one QPSK channel + gaussian noise). However, in real-life scenarios I either get a few channels or a lot of false-positives. Of course I can fix this by fine-tuning the weights in the threshold calculation, but then it wouldn't be automatic anymore.

I've been doing some research on Google but maybe I'm not using the right search keywords, or there is no real interest on this subject (which would be strange, as frequency scanners must perform this task somehow).

How could I find the appropriate values for the mean weights? Maybe there is a better approach than calculating a threshold for the whole spectrum?

BatchDrake
  • 41
  • 2
  • How frequently are you sampling? (Or, equivalently: how much time is spanned by your 1024 samples?) I assume you're already familiar with the Nyquist sampling theorem? (I ask because L-band signals are upwards of 1 GHz, so if you're taking frequent enough samples to avoid running afoul of that theorem, then 1024 samples would span less than a microsecond, so it seems like you can probably get some improvement by just taking more samples over more time.) – ruakh Mar 26 '17 at 19:46
  • 250000 for this particular case, and my unit tests don't take the sample rate into account, they just run in normalized frequencies. Anyways, don't worry about it too much, the SDR I'm using (Nuand's bladeRF) is able to tune to any frequency between VHF and 6 GHz, so I don't have to sample the whole spectrum (that would be crazy!). In the end, it's like sampling a baseband signal between -125000Hz and 125000 Hz. – BatchDrake Mar 26 '17 at 22:34
  • The smoothing sounds counter-productive. There's no reason noise would look like a peak in frequency domain. – Marcus Müller Mar 27 '17 at 11:35

2 Answers2

0

Your smoothing approach seems a bit counter-productive: Why would noise in a sufficiently long DFT form something like a "sharp" shape? You're most likely suppressing narrowband carriers that way.

Then: There's really a lot of signal detectors, many simply based on energy detection in spectrum (just like your approach).

However, the better an estimator has to be, the more info on the signal you're looking for you'll need to have. Are you looking for 20 kHz wide narrowband channels, or for dozens of Megahertzes of high-rate QPSK satellite downlink? Do you know anything about the channel/pulse shape? Autocorrelation?

This is a bit of a wide field, so I'd propose you look at something that already works:

gr-inspector of Sebastian Müller is a proven and pretty awesome automatic channel detector, and can, for some types of transmissions, also infer modulation parameters.

See a demo video (dubstep warning!); what you seem to be sketching looks something like this in that:

gr-inspector energy detection

But: that's just one of the things it can do.

More importantly: Energy-band detection based on a DFT is really just one of many things you can do to detect signals. It wouldn't, for example, detect spread-spectrum transmissions, like GPS, for example. For that, you'd need knowledge of the spreading technique or at least a large autocorrelation-based detector.

Marcus Müller
  • 34,677
  • 4
  • 53
  • 94
  • I'm not smoothing in frequency but in time. For each calculation of the power spectrum (say `spec`) I update my spectrogram like this: `for (i = 0; i < LEN; ++i) spectrogram[i] += alpha * (spec[i] - spectrogram[i]);` I'm looking for anything that looks like a PSK channel, of any bandwidth that can fit in my spectrogram, this first step doesn't have to be too accurate. After having a hint of the channel's location, I can center the spectrum, do some filtering / decimation and apply other techniques to guess the actual channel parameters (baudrate, etc). – BatchDrake Mar 27 '17 at 14:26
  • Hm, in that case, would something that would only have an exponential decay instead of being a linear IIR low pass not perform better at detecting packetized links? – Marcus Müller Mar 27 '17 at 15:50
  • I'm afraid I don't follow you. Why does an exponential decay make sense here? Could you please elaborate? – BatchDrake Mar 27 '17 at 16:33
  • In real world, you get things that are packeted – so they're just there for a short "blip". So, your filter should let short "rises" in power in a frequency bin through. However, if it was a proper linear filter, the output would fall just as quick – and we don't want that, instead, we'd want to be able to rise quick, and then fall slowly (at first, and then accelerate if it really was just a single burst) – Marcus Müller Mar 27 '17 at 16:48
  • Look at the top half of this gr-fosphor visualization to get a graphical idea of what I mean: https://www.youtube.com/watch?v=mjD-l3GAghU – Marcus Müller Mar 27 '17 at 16:49
  • Okay, I see what you mean. This is basically a peak hold with exponential decay, right? I rarely see burst transmissions in this band, but this may help me calculate an estimation of the noise floor. Also, I've just tried gr-inspector and it turns out that even though the signal detector block has an "auto-threshold" functionality, it doesn't seem to work with my captures (and if I force the threshold, I'm facing again the problem of false positives/negatives). – BatchDrake Mar 27 '17 at 17:47
  • true, peak hold with exp decay. Well, there's no remedy for false positives / missed transmission when working with thresholds – stochastics doesn't allow that. – Marcus Müller Mar 27 '17 at 17:50
  • – and to do "better" than "dumb" thresholds, you'd have to have more knowledge on the signals of interest's behaviour – Marcus Müller Mar 27 '17 at 17:57
0

After considering Marcus Müller's suggestion, I developed an algorithm that still relies on a global energy threshold but also on an estimation of the noise floor. It can be improved in many ways, but as its simplest realization already provides acceptable results with real-world captures (again, L-band captures at 250 ksps) I believe it could be a good ground for other people to start with.

The central idea of the algorithm is to calculate the energy threshold based on a continuous estimation of the noise power, updating it with every update of the spectrogram. This estimation keeps track of the maximum and minimum levels attained by each FFT bin after during all FFT runs, using them to estimate the PSD in that bin and discard outliers (i.e. channels, spurs...). The algorithm is to be executed every fixed number of samples. More formally:

Parameters of the algorithm:

int N        /* spectrogram length (FFT bins) */
complex x[N] /* last N samples */
float alpha  /* spectrogram smoothing factor between 0 (infinite smoothing,
                spectrogram will never be updated) and 1 (no smoothing at all) */
float beta   /* level decay factor between 0 (levels will never decay) and 1
                (levels will be equal to the spectrogram). */
float gamma  /* smooth factor applied to updates of the current noise estimation
                between 0 (no updates allowed) and 1 /* no smoothing */
float SNR    /* detection threshold above noise, in dB */

Recommended values for alpha, beta and gamma are 1e-2, 1e-3 and .5 respectively.

Variables:

complex dft[N]  /* Fourier transform of the last N samples */
float spect[N]  /* smoothed spectrogram */
float spmin[N]  /* lower levels for spectrogram bins */
float spmax[N]  /* upper levels for spectrogram bins */
int runs        /* FFT run counter (initially zero) */
float N0        /* Current noise level estimation */
float new_N0    /* New noise level estimation */
float min_pwr   /* Minimum power density found */
int min_pwr_bin /* FFT bin where min_pwr is */
int valid       /* Number of valid bins for noise estimation */
int min_runs    /* Minimum number of runs required to detect channels */

Algorithm:

min_runs = max(2. / alpha, 1. / beta)
dft = FFT(x);
++runs;
if (runs == 1) then /* First FFT run */
    spect = dft * conj(dft) /* |FFT(x)|^2 */
    spmin = spect /* Copy spect to spmin */
    spmax = spect /* Copy spect to spmax */
    N0    = min(spect); /* First noise estimation */
else
    /* Smooth spectrogram w.r.t the previous run */
    spect += alpha * (dft * conj(dft) - spect)

    /* Update levels. This has to be performed element-wise  */
    new_N0 = 0
    valid = 0
    min_pwr = INFINITY
    min_pwr_bin = -1
    for (int i = 0; i < N; ++i)
        /* Update current lower levels or raise them */
        if (spect[i] < spmin[i]) then
            spmin[i] = spect[i]
        else
            spmin[i] += beta * (spect[i] - spmin[i]);
        end

        /* Update current upper levels or decrease them */
        if (spect[i] > spmax[i]) then
            spmax[i] = spect[i]
        else
            spmax[i] += beta * (spect[i] - spmax[i]);
        end

        if (runs > min_runs) then
            /* Use previous N0 estimation to detect outliers */
            if (spmin[i] < N0 or N0 < spmax[i]) then
                new_N0 += spect[i]
                ++valid
            end

            /* Update current minimum power */
            if (spect[i] < min_pwr) then
                min_pwr = spect[i]
                min_pwr_bin = i
            end
        end
    end

    /*
     * Check whether levels have stabilized and update noise
     * estimation accordingly
     */
    if (runs > min_runs) then
        /*
         * This is a key step: if the number of valid bins is
         * 0 this means that our previous estimation was
         * absolutely wrong. We reset it with a cruder estimation
         * based on where the minimum value of the current
         * spectrogram was found
         */

        if (valid == 0) then
            N0 = .5 * (spmin[min_pwr_bin] + spmax[min_pwr_bin])
        else
            N0 += gamma * (new_N0 / valid - N0)
        end

        /*
         * Detect channels based on this threshold (trivial,
         * not detailed)
         */

        detect_channels(spect, 10^(SNR / 10) * N0)
    end
end

Even though this algorithm makes the strong assumption that the noise floor is flat (which is false in most cases as in real-world radios, tuner output passes through a low-pass filter whose response is not flat), it works even if this condition doesn't hold. These are some of the algorithm results for different values of alpha, N = 4096 and SNR = 3 dB. Noise estimation is marked in yellow, channel threshold in green, upper levels in red, spectrogram in white and lower levels in cyan. I also provide an evolution of the N0 estimation after every FFT run:

Results for alpha = 1e-1: Results for alpha = 1e-1

Results for alpha = 1e-2. Note how the number of valid bins has been reduced as the spectrogram got clearer: Results for alpha = 1e-2

Results for alpha = 1e-3. In this case, the levels are so tight and the noise floor so obviously non-flat that there are novalid bins from one FFT run to another. In this case we fall back to the crude estimation of looking for the bin with the lowest power density:

Results for alpha = 1e-3 The min_runs calculation is critical. To prevent the noise level to updrift (this is, to follow a channel and not the noise floor) we must wait at least 2. / alpha FFT runs before trusting the signal levels. This value was found experimentally: in my previous implementations, I was intuitively using 1. / alpha which failed miserably for alpha = 1e-3:

Updrift

I haven't tested this yet on other scenarios (like burst transmissions) where this algorithm may not perform as well as with continuous channels because of the persistence of min/max levels, and it may fail to detect burst transmissions as outliers. However, given the kind of channels I'm working with, this is not a priority for now.

BatchDrake
  • 41
  • 2