User Windows - QuantAsylum/QA40x GitHub Wiki

Windows in digital signal processing (DSP) play a crucial role in managing the spectral characteristics of a signal during various operations, such as the Fourier Transform. Essentially, a window function is applied to a signal to reduce the effects of spectral leakage—a phenomenon where energy from one frequency component leaks into other frequencies, distorting the true spectral content. When a segment of a signal is analyzed or processed, it is effectively multiplied by a window function, which typically has a value of 1 (or close to 1) in the middle and tapers off to zero at the edges. This tapering minimizes the abrupt discontinuities at the boundaries of the segment. Common window functions used in DSP include the Rectangular, Hamming, Hanning, and Blackman windows, each with its own characteristics and applications. The choice of window affects the balance between frequency resolution and leakage, with some windows offering better frequency resolution and others providing reduced spectral leakage, making the selection of an appropriate window a critical decision in DSP applications.

There are dozens if not hundreds of windows that have been developed over the user. Often times, they are tweaks on other windows to improve the performance in certain areas. Wikipedia has a good article (HERE) on Window Functions and the tradeoffs and performance of each.

The QA40x software supports user-defined windows, allowing you to create arbitrary window functions you have developed yourself, or that you might have encountered in research. Let's take a look at the section titled Hann and Hamming Windows from the Wiki article. Note the generalized case for both Hann and Hamming appears as follows:

image

The article explains that a Hamming Window can be had by setting the $a_0$ term to 0.54.

Realizing this function in code

Beginning with release 1.196, you have the ability to write your own windowing function. This is accomplished by creating a text file with the extension *.cs and providing three functions (or methods) inside that file. In the MyDocs\QuantAsylum\QA40x\UserWindows directory you'll find a file called Hamming.cs which implements the Hamming function shown above. The contents of that file are shown below:

public double GetAmplitudeCorrection()
{
	return 1.850;
}

public double GetEnergyCorrection()
{
	return 0.857;
}

public double[] ApplyWindowToTimeSeries(double[] timeSeries)
{
	double[] r = new double[timeSeries.Length];
	
	double omega = 2.0 * PI / (timeSeries.Length);
	for (int i = 0; i < timeSeries.Length; i++)
	{
		r[i] = (0.54 - (1 - 0.54) * Cos(omega * i)) * timeSeries[i];
	}
	
	return r;
}

The first function is called GetAmplitudeCorrection() and this provides an empirically derived value that will allow the QA40x application to correctly adjust the displayed amplitude. The second function is called GetEnergyCorrection() and this provides an empirically derived value that will allow the QA40x application to adjust the displayed energy.

These are both required because we are "shaping" the acquired waveform using the window function. This has the effect of reducing the waveform amplitude at most locations, and as a result, the energy and average amplitude of the waveform is reduced. The correction factors above allow us to restore the amplitude and energy measurements as if the acquired tone had not be windowed at all. So, we apply the window to control the spectral leakage, and then we correct the amplitude and energy measurements to compensate for the window we applied to control the leakage.

Below, we'll discuss how to empirically determine these correction factors for any window you might encounter.

Let's re-visit the wiki-provided representation of a Hamming window, and compare that to the source code that implements the Hamming window.

image

public double[] ApplyWindowToTimeSeries(double[] timeSeries)
{
	double[] r = new double[timeSeries.Length];
	
	double omega = 2.0 * PI / (timeSeries.Length);
	for (int i = 0; i < timeSeries.Length; i++)
	{
		r[i] = (0.54 - (1 - 0.54) * Cos(omega * i)) * timeSeries[i];
	}
	
	return r;
}

In the source, we've defined a variable name omega that captures the 2*PI/N inside the Cos() term, where N is the length of the time series. Then we iterate over the time series, using the equation from Wiki with $a_0$ set to 0.54.

In the QA40x application, we can load this windowing function by right-clicking on the USER Window button.

image

This will open a dialog that allows us to selection a *.cs file. If the file is loaded and compiled successfully, we'll see the following:

image

If there is an error in compilation, you'll see the first error message displayed in the dialog. For example, let's delete the semicolon after the return r statement in the ApplyWindowToTimeSeries() function. Note that we now have an error from the compiler indicating an semicolon was expected:

image

Under the Hood

The QA40x application is written in C#, and the C# runtime has the ability to compile C# code. Inside the QA40x application is a class that is specified as follows:


namespace QA40x.UserWindow
{
    public class UserWindowRuntimeEnvironment : IUserWindow
    {
        double PI
        {
            get
            {
                return System.Math.PI;
            }
        }

        double Sin(double x)
        {
            return System.Math.Sin(x);
        }

        double Cos(double x)
        {
            return System.Math.Cos(x);
        }

        /*USERCODE*/
    }
}

Where the comment /USERCODE/ is located is where the code you write will be substituted. The class has IUserWindow as its base interface class. Being an interface, this means you are required to implement the three specified functions. The IUserWindow is shown below:

 public interface IUserWindow
    {
        double GetAmplitudeCorrection();

        double GetEnergyCorrection();

        double[] ApplyWindowToTimeSeries(double[] timeSeries);
    }

When you load the *.CS file, the contents of that file are merged into the UserWindowRuntimeEnvironment class, and then the class is compiled. And if successfully compiled, an instance is created, and that instance is used for the custom window processing. The process of compiling is fast, at about 100 mS. And once compiled an executed for the first time, the function runs as fast as the native methods for other window processing. In all, it's a very flexible mechanism.

Security Considerations

You might notice the UserWindowRuntimeEnvironment provides Sine, Cosine and PI functions. This might seem odd if you are used to C#. However, the reason for this is because we don't want to run just any code from inside the class fragment we are loading. The QA40x application will not allow you to import ``SystemorMicrosoft``` namespaces (and a few others), which means the User Window code isn't able to perform networking, thread operation, file operations, etc. It's not bulletproof, however. This means you should exercise caution when it comes to running a User Window someone might have given you. Open the ```*.cs``` file in a text editor. If it's using functions you aren't familiar with, don't run it.

Determining Amplitude and Energy Corrections

As discussed previously, windowing functions require corrections to subsequent calculations on the waveform because the windowing functions modify the waveform to reduce spectral leakage. Go back to the hamming.cs code and change the two functions related to these corrections as follows:

public double GetAmplitudeCorrection()
{
	//return 1.850;
        return 1.0;
}

public double GetEnergyCorrection()
{
	//return 0.857;
        return 1.0;
}

In the code above, we've reverted to the factor 1.0 which won't give us any correction. We can now re-load the window using a sine in loopback set to 0 dBV. Make sure that your are using the "Round to eliminate leakage" option in the generator settings. This is because we want the generated sine to be in the middle of an FFT bin.

The screen shot is as follows. Note that the RMS Volts and PkVrms readings are wrong when the USER (Hamming) window is being used. If we switch to other windows, such as FlatTop and Hann, the values are correct. Our task here is to determine the corrections required.

image

From the plot above, we can see the measured peak is reported at 540.1 mVrms. The inverse of this is 1.85. So, restore that value in the GetAmplitudeCorrection() function:

public double GetAmplitudeCorrection()
{
	return 1.850;
	//return 1.0;
}

Reload the Window, and note how the Peak Vrms is now correct, and the displayed peak is also correct. But the RMS (energy) reading is not. It's at 1.166V, but it should be 1.000. This suggests a correction of $1/1.166$, or 0.857.

image

Restoring that value into the code yields the following.

public double GetEnergyCorrection()
{
	return 0.857;
	//return 1.0;
}

And a reload of the window with both corrections updated yields the following:

image