Published:
March 22, 2005
By Richard G. Baldwin
Java Programming, Notes # 1488
Miscellaneous DSP topics
Other previous lessons in this series, going all the way back to the lesson entitled Plotting Engineering and Scientific Data using Java have taught you quite a lot about the use of Java for Digital Signal Processing (DSP).
The following lessons have taught you about the topics indicated in the titles:
Spectral analysis
Numerous other lessons have taught you about spectral analysis. For example, several previous lessons, including the lesson entitled Spectrum Analysis using Java, Sampling Frequency, Folding Frequency, and the FFT Algorithm, have discussed spectral analysis in detail.
Matched Filtering
This lesson will introduce you to matched filtering. The lesson will teach you how to use convolution in Java to perform matched filtering, and will provide examples of matched filtering using Java.
The lesson will demonstrate that the use of matched filtering often makes it possible to detect properly designed signals in noisy environments where simple frequency filtering alone cannot do the job.
Viewing tip
You may find it useful to open another copy of this lesson in a separate browser window. That will make it easier for you to scroll back and forth among the different figures and listings while you are reading about them.
Supplementary material
I recommend that you also study the other lessons in my extensive collection of online Java tutorials. You will find those lessons published at Gamelan.com. However, as of the date of this writing, Gamelan doesn't maintain a consolidated index of my Java tutorial lessons, and sometimes they are difficult to locate there. You will find a consolidated index at www.DickBaldwin.com.
The purpose of this lesson is to help you understand the use of convolution for matched filtering.
I won't attempt to explain the theoretical basis for convolution. If you search the web using Google, you will undoubtedly find numerous links to theoretical discussions on convolution.
I explained the mechanics of convolution in the previous lesson entitled Convolution and Frequency Filtering in Java, so I won't repeat that discussion here.
Signals in noise
Many situations involve dealing with signals buried in noise. In some situations, such as tuning a radio or television receiver, the objective is to extract the signal from the noise so that something can be done with it, (such as listening to the music).
This is not a situation where matched filtering can be particularly helpful, particularly if the system is all analog.
Detection systems
In other situations, such as SONAR, RADAR, and possibly network cables, the objective is to determine the presence or absence of a signal. Determining the presence or absence usually involves detecting the signal.
SONAR
For example, an active SONAR system sends acoustic pulses into the water and then listens for echoes produced when the acoustic energy bounces off of something like a submarine. If the system detects an echo, that usually means that there is something in the area that is reflecting the acoustic pulses.
RADAR
Similarly, a RADAR system sends electromagnetic pulses into the air and listens for echoes from airplanes (or speeding automobiles). If echoes are detected, that usually means that there is something in the area that is reflecting the electromagnetic pulses.
Network cable receiver
A receiver on the end of a network cable may listen for electromagnetic or optical pulses sent by a transmitter on the other end of the cable. The fact that such pulses are detected is provided to other parts of the system capable of decoding the information that was encoded in those pulses.
Matched filtering for detection
Matched filtering is often used in situations where it is desired to detect the presence or absence of pulses of a known waveform buried in noise. I will present and explain a program in this lesson that simulates such a situation.
Frequency filtering
One of the early stages in most systems designed to extract or detect signals in noise is to apply a frequency filter to eliminate the energy at all frequencies outside the frequency band occupied by the signal. This is what you are doing when you tune your radio receiver to a particular radio station. Hopefully, you are eliminating the energy being transmitted by all other radio stations that operate on different frequencies.
If the data involved is digital data, digital convolution is one approach that can be used for such frequency filtering. I explained this approach in the previous lesson entitled Convolution and Frequency Filtering in Java.
Matched filtering goes further
Matched filtering goes further than simple frequency filtering. Although matched filtering does make use of information regarding the operating frequency of the source of the signal, it also takes advantage of the complex spectrum and phase information contained in the signal.
(I explained these concepts in the earlier lesson entitled Spectrum Analysis using Java, Complex Spectrum and Phase Angle.)
The complex spectrum and phase characteristics of a time-domain pulse manifest themselves in the waveform of the pulse. Matched filtering is a time-domain process that takes advantage of the waveform of the signal pulse. This in turn takes advantage of the complex spectral and phase information that describes the pulse.
Benefits of matched filtering
The use of matched filtering often makes it possible to detect the presence of properly designed signal pulses in noisy situations where simple frequency filtering alone cannot do the job. This will be illustrated by one of the sample programs in this lesson.
(This lesson does not go into design issues for creating a properly designed pulse that works well with matched filtering. Generally speaking, such pulses need to have a large time-bandwidth product.)
I will present and explain two programs in this lesson. The first program named Dsp040a illustrates an implementation of matched filtering in the absence of noise. This program is useful in showing you how matched filtering achieves the benefits that it provides.
The second program named Dsp040b illustrates an implementation of matched filtering for a signal pulse buried deep in noise. Simple frequency filtering is performed first to illustrate that simple frequency filtering in this case is not sufficient for detection of the signal. Then matched filtering is applied, followed by an automatic detection process. Even though the signal-to-noise ratio is fairly low, the process can usually detect the presence of the signal.
The program named Graph03
The program named Graph03 is a utility plotting program from a series of programs first discussed in the lesson entitled Plotting Engineering and Scientific Data using Java. I will refer you to that lesson for a detailed discussion of the concepts involved.
A listing of the Graph03 program is provided in Listing 17. Similarly, a listing of a required interface named GraphIntfc01 is provided in Listing 18.
The program named Dsp040a
This program illustrates the use of convolution for matched filtering. In particular, it is designed to show the behavior of a matched filter process in the absence of noise.
An infinite signal-to-noise ratio
The program begins by populating a 400-sample noise array with a zero for every noise value. This later results in an infinite signal-to-noise ratio when a signal pulse is added into the array.
Linear sweep FM signal pulse
Then the program creates a linear sweep FM signal pulse. The frequency sweep extends from zero to 3300 Hz at a sampling rate of 16000 samples per second. The pulse has a constant peak amplitude throughout the length of the pulse.
Signal plus noise
Then the signal pulse is added to the noise beginning at sample index 200. This results in an infinite signal-to-noise ratio because all noise values are zero.
Perform a matched filter operation
After that, the signal plus noise is convolved with a replica of the signal to implement the matched filter process.
Compute the spectral content
Then a spectral analysis is performed on the raw signal plus noise to show the frequency spectrum of the signal.
(The spectrum isn't impacted by the noise because all the noise values are zero.)
Because a replica of the signal is used as the convolution operator, the frequency spectrum of the signal is also the frequency response of the convolution operator.
Plot the results
Then the program plots the following curves:
Miscellaneous
This program requires access to the interface named GraphIntfc01.
The program was tested using JDK 1. 4. 2 under WinXP.
You can run this program by entering the following command at the command-line prompt.
java Graph03 Dsp040a
The program output
The graphic output produced by this program is shown in Figure 1.
Figure 1 Output from program Dsp040a |
The signal pulse
The first plot down from the top in Figure 1 shows the signal pulse added to noise where all of the noise values are zero.
(In the next program, I will add the same signal to noise where the level of the noise is substantial. In this program, I wanted to be able to show you how matched filtering behaves without having to deal with the impact of noise on the situation.)
The signal spectrum
The bottom curve in Figure 1 shows the spectral content of the signal from zero to the sampling frequency.
(Recall that the amplitude spectrum of a real time series is always symmetric about the folding frequency, which is half the sampling frequency. See the previous lesson entitled Spectrum Analysis using Java, Sampling Frequency, Folding Frequency, and the FFT Algorithm.)
The area of interest is the area on the left from zero to the folding frequency, midway between the two ends of the spectral plot. Under the assumption that the sampling frequency is 16000 samples per second, the folding frequency is at 8000 Hz.
The convolution operator
The second plot down from the top in Figure 1 shows a replica of the signal pulse. The pulse in the second plot will be used as a convolution operator. Because it is a replica of the signal for which the spectral plot was computed, the spectral plot at the bottom represents the frequency response of the convolution operator.
Pass and reject frequency bands
The pass band for this filter extends from zero to approximately 6000 Hz. The reject band extends from approximately 6000 Hz to 8000 Hz, which is the folding frequency.
(This frequency response information will be important in the next program. We will use the convolution filter to limit the spectral content of the noise to that exhibited by the signal before adding the signal to the noise. This will eliminate or greatly reduce any possibility of detecting the signal in the noise based on frequency filtering alone.)
Matched filtering through convolution
The convolution operator will be convolved with the signal plus noise in the first plot, producing the convolution output shown in the third plot. Because the convolution operator is a replica of the signal waveform, the convolution process is also a matched filter process. In other words, the convolution operator matches the complex spectral and phase (waveform) characteristics of the signal.
Time compression
Pay particular attention to the difference between the waveform of the signal in the first plot and the waveform of the matched filter output in the third plot.
The signal in the first plot is 50 samples in length. However, all of the energy in those 50 samples has been compressed into perhaps ten samples in the output of the matched filter in the third plot.
Furthermore, the waveform of the matched filter output in the third plot is symmetrical about its peak. Also, the peak in the matched filter output occurs at the beginning of the signal in the first plot. In other words, the matched filter output does a good job of locating the position of the signal in the first plot.
The bottom line
Convolving a properly designed signal with an operator that is a replica of the signal causes all of the energy in the signal to be time-compressed into a very narrow and very tall waveform.
(If plotted at the same scale, the waveform in the third plot would be much taller than the waveform in the first plot.)
Some signal waveforms do a much better job of time compression than others. Generally, the wider the bandwidth of the signal, the narrower will be the matched filter output. The longer the signal extends in time, the taller will be the match filter output. Thus, a properly designed pulse for matched filtering usually has a large time-bandwidth product.
Benefits of time compression
Time compression can have a very beneficial effect when trying to detect pulses buried in noise. As we will demonstrate in the next program, converting the long low signal buried in the noise to a narrow tall signal buried in the same noise will often cause the signal to extend above the noise and make it detectable, when it would not be detectable otherwise.
Discussion of the code for Dsp040a
I will discuss the code in fragments. You can view the entire program in Listing 15.
The class definition for Dsp040a begins in Listing 1. Note that this class implements GraphIntfc01. This is necessary to satisfy the requirements of the utility plotting program named Graph03.
class Dsp040a implements GraphIntfc01{ int signalLen = 50; int noiseLen = 400; int outputLen = noiseLen - signalLen; int spectrumPts = outputLen; double[] noise = new double[noiseLen]; double[] signal = new double[signalLen]; double[] output = new double[outputLen]; double[] spectrumA = new double[spectrumPts]; Listing 1 |
Listing 1 establishes the lengths for several arrays, and then creates the array objects. These array objects are used later to store different kinds of data, generally indicated by the names of the references to the array objects.
Note that the elements in all four of these arrays, and particularly the array referred to by noise, are initialized to their default values of 0.0.
The constructor
The constructor begins in Listing 2.
public Dsp040a(){//constructor fmSweep(signalLen,signal); Listing 2 |
The fmSweep method
The code in Listing 2 invokes the method named fmSweep to create a linear FM sweep signal where the frequency sweep extends from zero to 3300 Hz at a sampling rate of 16000 samples per second. The peak amplitude of the pulse is the same from the beginning to the end.
I explained this method in detail in the previous lesson entitled Convolution and Frequency Filtering in Java. Therefore, I won't discuss it further in this lesson. You can view the method in Listing 15.
A convolution operator
As described earlier, in addition to being used as the signal, this pulse will also be used as a convolution operator to perform a matched filter operation on the signal plus noise.
The convolution operator is shown in the second plot in Figure 1.
Add the signal to the noise
Listing 3 adds the signal into the empty noise array, beginning at sample index 200. The result is shown in the first plot in Figure 1.
for(int cnt = 0; cnt < signalLen; cnt++){ noise[cnt + 200] += signal[cnt]; }//end for loop Listing 3 |
Because all of the noise values in the array were initialized to zero, this results in an infinite signal-to-noise ratio.
Perform the convolution
Listing 4 invokes the static convolve method of the Convolve01 class to apply the convolution operator in the second plot in Figure 1 to the signal plus noise in the first plot in Figure 1.
Convolve01.convolve(noise,signal,output); Listing 4 |
Because the noise values are all zero, this amounts to applying the convolution operator to the signal alone. The convolution operator is a replica of the signal. Thus, this operation applies a matched filter to the signal, producing the time-compressed output shown in the third plot in Figure 1.
I explained the convolve method in detail in the previous lesson entitled Convolution and Frequency Filtering in Java so I won't discuss it further here. You can view the class named Convolve01 in Listing 15.
Compute the frequency response of the convolution filter
Listing 5 invokes the static dft method of the Dft01 class to compute the spectrum of the signal plus noise shown in the first plot in Figure 1.
Dft01.dft(noise,spectrumPts,spectrumA); }//end constructor Listing 5 |
Because the noise values are all zero, and because the convolution operator is a replica of the signal pulse, this also computes the frequency response of the convolution operator. The result is shown in the bottom plot in Figure 1.
I have explained the method named dft in several previous lessons, so I won't discuss it further here. You can view the Dft01 class in Listing 15.
End of the constructor
Listing 5 also signals the end of the constructor.
At this point, the data for all four plots has been computed and saved in the arrays declared early in the program. That data can be accessed and plotted at this time.
Plotting the data
The program named Graph03 invokes the methods named f1, f2, f3, and f4 to access the data for plotting.
(You can view the program named Graph03 in Listing 17.)
I explained a program very similar to Graph 03 and the methods named f1 through f5 in the lesson entitled Plotting Engineering and Scientific Data using Java. Therefore, I won't repeat that explanation here. You can view the methods in Listing 15. You can view the program named Graph03 in Listing 17, and you can view the interface named GraphIntfc01, which declares the methods, in Listing 18.
That concludes the discussion of the program named Dsp040a.
The program named Dsp040b
Now for something a little more exciting. This program illustrates the use of convolution for matched filtering in the presence of noise. The program buries a signal in noise having the same bandwidth as the signal. Then it uses a matched filter to detect that signal.
Changing the signal-to-noise ratio
The program is designed to make it easy to experiment with signal detectability versus signal-to-noise ratio. You can determine the success or lack thereof in detecting a signal at a different signal-to-noise ratio by changing the value of one variable named SNR and then recompiling and rerunning the program.
Generate some white noise
The program begins by creating 400 samples of white noise using a random noise generator.
(I discussed the concept of white noise in detail in the previous lesson entitled Convolution and Frequency Filtering in Java.)
Create a signal pulse
Then the program creates a linear sweep FM signal pulse with the frequency range extending from zero to 3300 Hz at a sampling rate of 16000 samples per second. The pulse has a constant peak value throughout the length of the pulse.
Shaping the noise spectrum
Following this, the program uses the signal pulse as a convolution operator to filter the white noise and to eliminate noise energy that is outside the frequency band of the signal. This is done so that most of the improvement in signal detectability that is achieved later through matched filtering will be due solely to matched filtering. Little or none of the improvement will be due to simple frequency filtering.
Adjusting the noise amplitude
After the noise has been filtered for bandwidth control, it is scaled to a peak value of 1.0. This is done so that it will be possible to establish a definitive peak signal to peak noise ratio later.
Combining the signal and the noise
Then the signal pulse is scaled to a peak value of 0.35 and added to the scaled noise at sample index 200. This results in a peak signal to peak noise ratio of 0.35.
Adjusting the signal-to-noise ratio
The scale factor named SNR that is applied to the signal at this point can be modified to increase or decrease the signal-to-noise ratio. This is useful for experimenting with signal detectability versus signal-to-noise ratio.
Perform the matched filter process
After the signal is added to the noise, the signal plus noise data is convolved with a replica of the signal to perform the matched filter process.
Automatic peak detection
A peak detector is then applied to the filtered signal plus noise to automatically find the peak value in the matched filter output. The location and size of the peak is displayed on the screen. The location of the peak is marked in an empty array, which will later be displayed below a plot of the filtered signal plus noise.
Plot the results
Then the program plots the following curves as shown in Figure 2:
Miscellaneous
This program requires access to the interface named GraphIntfc01.
The program was tested using JDK 1.4.2 under WinXP.
Running the program
You can run this program by entering the following command at the command-line prompt:
The program output
The graphic output produced by this program is shown in Figure 2.
Figure 2 Output for program Dsp040b |
The signal plus noise data
The first plot from the top in Figure 2 shows the signal plus noise data. The second plot in Figure 2 is a replica of the signal that was added to the noise to produce the data in the first plot.
(Note that the first two plots in Figure 2 were plotted at the same scale. Therefore, the amplitude of the signal replica in the second plot is correct relative to the amplitude of the noise in the first plot.)
The signal was added to the noise beginning at sample index 200, which corresponds to the tenth tick mark counting from the left.
As you can see, the signal is completely lost in the noise.
Frequency filtering won't help much
Recall that the spectral content of the noise was preconditioned to cause it to match the spectral content of the signal. Therefore, there is little if any improvement in signal detectability that can be achieved at this point through additional simple frequency filtering. I will explain an easy way to demonstrate this later.
Looks like a job for a matched filter
The convolution operator shown in the second plot in Figure 2 was applied to the signal plus noise shown in the first plot, producing the output shown in the third plot. In other words, the third plot is the output from the matched filter.
(The plotting scale factor that was applied to the data in the matched filter output in the third plot was smaller by a factor of four than the scale factor that was applied to the first two plots. In other words, if the same scale factor had been used on the third plot, as was used for the first two plots, the peaks in the third plot would be four times larger. This would cause many of those peaks to extend completely out of the plotting area allocated to the third plot. Thus, the important thing to note is the signal-to-noise ratio in the third plot as compared to the signal-to-noise ratio in the first plot.)
The tallest peak
What we are interested in is the tallest peak in the third plot. As you can see, this peak occurs at the tenth tick mark. This is the location of the beginning of the signal in the first plot. Therefore, this peak was produced by applying the matched filter to the signal that was buried in the noise in the first plot. This peak represents the signal in the third plot.
Can easily be detected
This peak can easily be detected by eye at this relatively low signal-to-noise ratio. Thus, the use of a matched filter has caused this signal to become detectable in the third plot, when it was not detectable in raw form in the first plot.
An automatic peak detector
For even lower signal-to-noise ratios, the matched filter is not always successful in causing the largest peak in the third plot to correspond to the location of the signal in the first plot.
Even when successful for lower signal-to-noise ratios, the detection peak may be so close to the size of the other peaks that it may be difficult to identify the detection peak by eye.
Therefore, the fourth plot in Figure 2 contains a marker identifying the largest positive peak in the third plot. This can be very helpful in visually identifying the largest peak in the third plot.
Multiple pings
In a real SONAR or RADAR system, it is likely that multiple pings would be fired at the target with each ping producing an output similar to the third plot in Figure 2. However, most of the peaks other than the detection peak would likely be in different locations in the output for each successive ping.
Typically, additional ping-to-ping processing would be applied to make it possible to identify tracks produced by detection peaks in successive pings, some of which might be detectable and others of which might not be detectable.
The bottom line
The application of a matched filter to a properly designed signal pulse can cause the energy in the pulse to be compressed into a shorter time interval with a larger amplitude. This can often be useful in detecting the presence of such signals in noise at low signal-to-noise ratios.
Discussion of the code for Dsp039b
Once again, I will discuss the program in fragments.
The class definition for Dsp040b begins in Listing 6.
class Dsp040b implements GraphIntfc01{ int signalLen = 50; int noiseLen = 400; int outputLen = noiseLen - signalLen; int peakMarkerLen = outputLen; double[] noise = new double[noiseLen]; double[] signal = new double[signalLen]; double[] output = new double[outputLen]; double[] peakMarker = new double[peakMarkerLen]; Listing 6 |
This program starts out much the same way as the previous program. Listing 6 establishes the sizes of and create several arrays that will be used to hold various kinds of data throughout the program.
The constructor
The constructor begins in Listing 7.
public Dsp040b(){//constructor Random generator = new Random( new Date().getTime()); for(int cnt=0;cnt < noise.length;cnt++){ //Get noise and remove the dc offset. noise[cnt] = generator.nextDouble()-0.5; }//end for loop Listing 7 |
White random noise
Listing 7 uses a random number generator to get and save 400 samples of white random noise. The random number generator is fed a different seed each time the program is run, so the 400 random numbers will be different for each run of the program.
(Note that after you run the program for the first time from the command line, you can easily rerun it many times in succession simply by clicking the button labeled Graph in Figure 2. A new batch of noise data will be generated and processed each time you rerun the program.)
The code in Listing 7 is essentially the same as code that I explained in the previous lesson entitled Convolution and Frequency Filtering in Java. Therefore, I won't repeat that explanation here.
Need to limit the noise bandwidth
Because it is white noise, the noise at this point contains strong contributions of energy at all frequencies between zero and the folding frequency. Thus, it contains a lot of energy that is outside the bandwidth of the signal shown in Figure 1.
The noise data stored in the noise array at this point is not the noise that you see in the first plot in Figure 2. We need to limit the bandwidth of the noise to match the bandwidth of the signal in order to produce the noise that you see in the first plot. We will get to that in a moment. Right now, we need to generate the signal pulse.
Generating the signal pulse
The code in Listing 8 invokes the fmSweep method to generate a signal pulse identical to the signal pulse analyzed in the previous program named Dsp040a.
fmSweep(signalLen,signal); Listing 8 |
The code in Listing 8 is identical to the code in the previous program, so I won't discuss it further.
Note that a replica of this signal pulse will be used both to shape the bandwidth of the white noise, and to function as a matched filter for a signal that will be buried in the resulting noise.
Shaping the noise spectrum
Listing 9 uses a replica of the signal as a convolution filter to modify the spectrum of the white noise to make it match the spectrum of the signal. This produces all of the improvement in signal detectability that is available through simple frequency filtering.
Convolve01.convolve(noise,signal,output); Listing 9 |
Although this isn't necessary at this point, I wanted to take advantage of simple frequency filtering prior to setting up the matched filter experiment. That way, I can determine how much of the improvement in detectability is due to matched filtering.
Adjust the noise level
I also wanted to have definitive control over the peak signal to peak noise ratio. To accomplish this, I needed to control the peak level of the noise.
At this point, the filtered noise resides in the array object referred to by output. I need to copy it back into the array object referred to by noise. Along the way, I will adjust the amplitude of the noise so that the largest peak has an absolute value of 1.0. This is accomplished in Listing 10.
//Clear the noise array noise = new double[noiseLen]; //Get current max value. double max = 0.0; for(int cnt = 0;cnt < output.length;cnt++){ if(Math.abs(output[cnt]) > max){ max = output[cnt]; }//end if }//end for loop //Scale to a peak value of 1.0 for(int cnt = 0;cnt < output.length;cnt++){ noise[cnt] = output[cnt]/max; }//end for loop //clear the output array for future use output = new double[outputLen]; Listing 10 |
The code in Listing 10 is straightforward and is reasonably well commented. Now that you know what the code is intended to do, there should be no further need for an explanation.
Adjust the amplitude of the signal
Before adding the signal to the noise that now resides in the noise array, I need to adjust the amplitude of the signal so as to achieve a specific peak signal to peak noise ratio. This is accomplished in Listing 11.
double SNR = 0.35;//Signal-to-noise ratio for(int cnt = 0;cnt < signal.length;cnt++){ signal[cnt] = SNR*signal[cnt]; }//end for loop Listing 11 |
Why choose SNR equal to 0.35?
Through experimentation, I determined that for signal-to-noise ratios above 0.35, the process is almost always successful in detecting the signal in the noise.
At a signal-to-noise ratio of 0.35, the process is successful most of the time with an occasional failure to detect the signal.
As the signal-to-noise ratio goes below 0.35, the process fails to successfully detect the signal more frequently.
Therefore, I decided to set the peak signal to peak noise ratio to 0.35. This seemed to be a good balance between the two extremes of total success and total failure.
As originally generated, the signal pulse has a peak amplitude of 1.0. Therefore, since the noise also has a peak value of 1.0, I need to scale the signal values by 0.35 to achieve the desired peak signal to peak noise ratio. This is accomplished in Listing 11, and produces the replica of the signal shown in the second plot in Figure 2.
For future experiments
To experiment with signal detectability versus signal-to-noise ratio, all you need to do is to change the value of the variable named SNR and recompile the program.
Add the signal to the noise
Listing 12 adds the scaled signal to the noise beginning at a noise sample index of 200. This produces the data shown in the first plot in Figure 2. The signal begins at the tenth tick mark in Figure 1 and extends for 50 samples, or 2.5 tick marks.
for(int cnt = 0; cnt < signalLen; cnt++){ noise[cnt + 200] += signal[cnt]; }//end for loop Listing 12 |
In my opinion, even knowing where it was added, at a signal-to-noise ratio of only 0.35, the signal in the first plot is not visually distinguishable from the noise.
Apply the matched filter
Listing 13 uses the replica of the signal shown in the second plot in Figure 2 as a matched convolution operator, and convolves that operator with the data shown in the first plot, producing the output shown in the third plot.
Convolve01.convolve(noise,signal,output); Listing 13 |
Improved signal-to-noise ratio
As discussed earlier, the signal-to-noise ratio is dramatically improved in the third plot relative to the first plot. The signal is easily detectable in the third plot and is not detectable in the first plot.
Improvement is due to matched filtering
Since the bandwidth of the noise was previously shaped to the bandwidth of the signal, this improvement in peak signal to peak noise ratio and the attendant improvement in signal detectability was due almost exclusively to the use of a matched filter. An unmatched filter having the same frequency amplitude response would not provide an improvement of this magnitude.
An easy way to demonstrate this is to modify the program and flip the convolution operator end-for-end before passing it to the convolve method. It will still have the same amplitude frequency response shown in the last plot in Figure 1. However, the phase characteristics will no longer match the phase characteristics of the signal, and it will be totally ineffective in improving the signal-to-noise ratio and the detectability of the signal.
An automatic peak detector
The code in Listing 14 searches for the largest positive peak in the data shown in the third plot in Figure 2.
//Search for largest peak in the output int peakIndex = 0; max = 0.0; for(int cnt = 0;cnt < output.length;cnt++){ if(output[cnt] > max){ peakIndex = cnt; max = output[cnt]; }//end if }//end for loop //Display the location and value of the peak // on the screen System.out.println(peakIndex + " " + max); //Insert a marker in the peakMarker array // showing the location of the peak in the // filtered output. peakMarker[peakIndex] = 90; }//end constructor Listing 14 |
Once it finds the location of the peak, it displays the location and the value of the peak on the screen. Ideally for this setup, the location should always be 200. However, at this signal-to-noise ratio of 0.35 the process sometimes fails to detect the signal and the highest peak will occur at some location other than 200.
The code in Listing 14 also puts a marker at the location of the peak in the data plotted in the bottom plot in Figure 2.
The code in Listing 14 is straightforward, so I won't discuss it further.
The remaining code
Except for the use of different plotting scale factors in the interface methods, the remaining code in the program is code that I have previously discussed. Therefore, I won't discuss it further. You can view that code in Listing 16.
I encourage you to copy, compile, and run the programs that you will find in the listings near the end of the lesson. Modify them and experiment with them in order to learn as much as you can about the use of convolution for matched filtering.
I have suggested several possible experiments in the body of this lesson. You might want to try some of them.
I showed that the use of matched filtering often makes it possible to detect properly designed signals in noisy environments where simple frequency filtering alone cannot do the job. This was illustrated by one of the sample programs in this lesson.
A complete listing of each of the programs is provided in below.
/* File Dsp040a.java Copyright 2005, R.G.Baldwin This program illustrates the use of convolution for matched filtering. In particular, it is designed to show the behavior of the matched filter process in the absence of noise. The program begins by creating 400 samples of noise with every noise value equal to zero. This later results in an infinite signal-to-noise ratio. Then it creates a linear sweep FM signal pulse ranging from zero to 3300 Hz at a sampling rate of 16000 samples per second. The pulse has a constant peak value throughout the length of the pulse. Then the signal pulse is added to the noise at sample index 200. This results in an infinite signal-to-noise ratio because all noise values are zero. After that, the signal plus noise is convolved with a replica of the signal to implement the matched filter process. Then a spectral analysis is performed on the raw signal plus noise to show the frequency spectrum of the signal. Because a replica of the signal is used as the convolution operator, the frequency spectrum of the signal is also the frequency response of the convolution operator. Then the program plots the following curves: Signal plus noise Convolution operator Matched filter output Spectrum of raw signal plus noise Note: This program requires access to the interface named GraphIntfc01. Tested using JDK 1.4.2 under WinXP. ************************************************/ import java.util.*; class Dsp040a implements GraphIntfc01{ //Establish length for various arrays int signalLen = 50; int noiseLen = 400; int outputLen = noiseLen - signalLen; int spectrumPts = outputLen; //Create arrays to store different types of // data. double[] noise = new double[noiseLen]; double[] signal = new double[signalLen]; double[] output = new double[outputLen]; double[] spectrumA = new double[spectrumPts]; public Dsp040a(){//constructor //Note that the noise array contains all zero // values. //Create and save a signal pulse. This pulse // will also be used as the convolution // operator to operate as a matched filter. fmSweep(signalLen,signal); //Add the signal into the empty noise array // beginning at sample index 200. for(int cnt = 0; cnt < signalLen; cnt++){ noise[cnt + 200] += signal[cnt]; }//end for loop //Use the signal pulse as a convolution // operator and apply it to the data // consisting of signal plus noise. Recall // that the noise values are all zero in this // program. Convolve01.convolve(noise,signal,output); //Compute and save the DFT of the signal plus // noise, where the noise values are all // zero. Dft01.dft(noise,spectrumPts,spectrumA); //All of the time series have now been // produced and saved. They may be // retrieved and plotted by invoking the // methods named f1 through f5. }//end constructor //-------------------------------------------// //The following six methods are required by the // interface named GraphIntfc01. public int getNmbr(){ //Return number of functions to process. Must // not exceed 5. return 4; }//end getNmbr //-------------------------------------------// public double f1(double x){ int index = (int)Math.round(x); //This version of this method returns the // signal plus noise. if(index < 0 || index > noise.length-1){ return 0; }else{ //Scale for display and return. return noise[index] * 90.0; }//end else }//end f1 //-------------------------------------------// public double f2(double x){ //Return the signal pulse int index = (int)Math.round(x); if(index < 0 || index > signal.length-1){ return 0; }else{ //Scale for display and return. return signal[index] * 90.0; }//end else }//end f2 //-------------------------------------------// public double f3(double x){ //Return convolution output int index = (int)Math.round(x); if(index < 0 || index > output.length-1){ return 0; }else{ //Scale for display and return. return output[index] * 4.0; }//end else }//end f3 //-------------------------------------------// public double f4(double x){ //Return spectrum of signal plus noise int index = (int)Math.round(x); if(index < 0 || index > spectrumA.length-1){ return 0; }else{ //Scale for display and return. return spectrumA[index] * 15.0; }//end else }//end f4 //-------------------------------------------// public double f5(double x){ return 0.0; }//end f5 //-------------------------------------------// //This method generates a pulse with a linear // frequency sweep from zero to 3300 Hz at // a sampling rate of 16000 samples per // second. void fmSweep(int pulseLen,double[] output){ double twoPI = 2*Math.PI; double sampleRate = 16000.0; double lowFreq = 0.0; double highFreq = 3300.0; for(int cnt = 0; cnt < pulseLen; cnt++){ double time = cnt/sampleRate; double freq = lowFreq + cnt*(highFreq-lowFreq)/pulseLen; double sinValue = Math.sin(twoPI*freq*time); output[cnt] = sinValue; }//end for loop }//end method fmSweep }//end class Dsp040a //=============================================// //This class provides a static method named // convolve, which applies an incoming // convolution operator to an incoming set of // data and deposits the filtered data in an // output array whose reference is received as an // incoming parameter. class Convolve01{ public static void convolve(double[] data, double[] operator, double[] output){ //Apply the operator to the data, dealing // with the index reversal required by // convolution. int dataLen = data.length; int operatorLen = operator.length; for(int i = 0;i < dataLen-operatorLen;i++){ output[i] = 0; for(int j = operatorLen-1;j >= 0;j--){ output[i] += data[i+j]*operator[j]; }//end inner loop //Divide by the length of the operator }//end outer loop }//end convolve method }//end Class Convolve01 //=============================================// //This class provides a static method named dft, // which computes and returns the amplitude // spectrum of an incoming time series. The // amplitude spectrum is computed as the square // root of the sum of the squares of the real and // imaginary parts. //Returns a number of points in the frequency // domain equal to the number of samples in the // incoming time series. This is for convenience // only and is not a requirement of a DFT. //Deposits the frequency data in an array whose // reference is received as an incoming // parameter. class Dft01{ public static void dft(double[] data, int dataLen, double[] spectrum){ double twoPI = 2*Math.PI; //Set the frequency increment to the // reciprocal of the data length. This is // convenience only, and is not a requirement // of the DFT algorithm. double delF = 1.0/dataLen; //Outer loop interates on frequency values. for(int i = 0; i < dataLen;i++){ double freq = i*delF; double real = 0; double imag = 0; //Inner loop iterates on time- series // points. for(int j=0; j < dataLen; j++){ real += data[j]*Math.cos(twoPI*freq*j); imag += data[j]*Math.sin(twoPI*freq*j); spectrum[i] = Math.sqrt( real*real + imag*imag); }//end inner loop }//end outer loop }//end dft }//end Dft01 Listing 15 |
/* File Dsp040b.java Copyright 2005, R.G.Baldwin This program illustrates the use of convolution for matched filtering. It buries a signal in noise having the same bandwidth as the signal and then detects the signal using a matched filter. The program is designed so that experiments in signal detectability versus signal-to-noise ratio can be performed by changing the value of one variable and then recompiling the program. The program begins by creating 400 samples of white noise using a random noise generator. Then it creates a linear sweeped FM signal pulse ranging from zero to 3300 Hz at a sampling rate of 16000 samples per second. The pulse has a constant peak value throughout the length of the pulse. Then the program uses the signal pulse as a convolution operator to filter the white noise and to eliminate noise energy that is outside the frequency band of the signal. This is done so that all improvement that is achieved later through matched filtering will be due to matched filtering and none of the improvement will be due to simple frequency filtering. After the noise is filtered for bandwidth control, it is scaled to a peak value of 1.0. This is done so that it will be possible to establish a definitive peak signal to peak noise ratio later. Then the signal pulse is scaled to a peak value of 0.35 and added to the scaled noise at sample index 200. This results in a peak signal to peak noise ratio of 0.35. The scale factor that is applied to the signal can be modified to experiment with signal detectability versus signal-to-noise ratio. After that, the signal plus noise is convolved with a replica of the signal to implement the matched filter process. A peak detector is then applied to the filtered signal plus noise to find the peak value in the matched filter output. The location and size of the peak is displayed on the screen. The location of the peak is also marked in an empty array which will later be displayed below a plot of the filtered signal plus noise. Then the program plots the following curves: Raw signal plus noise Convolution operator Output from matched filter process Peak marker indicating the location of the highest positive peak in the output from the matched filter process Note: This program requires access to the interface named GraphIntfc01. Tested using JDK 1.4.2 under WinXP. ************************************************/ import java.util.*; class Dsp040b implements GraphIntfc01{ //Establish length for signal, noise, and // peak marker arrays int signalLen = 50; int noiseLen = 400; int outputLen = noiseLen - signalLen; int peakMarkerLen = outputLen; //Create arrays to hold various types of data. double[] noise = new double[noiseLen]; double[] signal = new double[signalLen]; double[] output = new double[outputLen]; double[] peakMarker = new double[peakMarkerLen]; public Dsp040b(){//constructor //Generate and save some wide-band random // noise. Seed with a different value each // time the object is constructed. Random generator = new Random( new Date().getTime()); for(int cnt=0;cnt < noise.length;cnt++){ //Get noise and remove the dc offset. noise[cnt] = generator.nextDouble()-0.5; }//end for loop //Create and save a signal pulse. This pulse // will also be used as the covlution // operator to operate as a matched filter. fmSweep(signalLen,signal); //Filter the noise to eliminate all energy // outside the band of the signal. This is // done so that all improvement later in the // matched-filter operation will be due to // matched filtering and none of the // improvement will be due to simple // frequency filtering. Convolve01.convolve(noise,signal,output); //Copy the filtered noise back into the noise // array, scaling it to a peak value of 1.0 //Clear the noise array noise = new double[noiseLen]; //Get current max value. double max = 0.0; for(int cnt = 0;cnt < output.length;cnt++){ if(Math.abs(output[cnt]) > max){ max = output[cnt]; }//end if }//end for loop //Scale to a peak value of 1.0 for(int cnt = 0;cnt < output.length;cnt++){ noise[cnt] = output[cnt]/max; }//end for loop //clear the output array for future use output = new double[outputLen]; //Scale the size of the signal. Change this // scale factor to experiment with // detectability versus signal-to-noise // ratio. double SNR = 0.35;//Signal-to-noise ratio for(int cnt = 0;cnt < signal.length;cnt++){ signal[cnt] = SNR*signal[cnt]; }//end for loop //Add the scaled signal into the white noise // beginning at sample index 200. for(int cnt = 0; cnt < signalLen; cnt++){ noise[cnt + 200] += signal[cnt]; }//end for loop //Use the signal pulse as a matched // convolution operator and apply it to the // data consisting of signal plus noise. Convolve01.convolve(noise,signal,output); //Search for largest peak in the output int peakIndex = 0; max = 0.0; for(int cnt = 0;cnt < output.length;cnt++){ if(output[cnt] > max){ peakIndex = cnt; max = output[cnt]; }//end if }//end for loop //Display the location and value of the peak // on the screen System.out.println(peakIndex + " " + max); //Insert a marker in the peakMarker array // showing the location of the peak in the // filtered output. peakMarker[peakIndex] = 90; //All of the time series have now been // produced and saved. They may be // retrieved and plotted by invoking the // methods named f1 through f4. }//end constructor //-------------------------------------------// //The following six methods are required by the // interface named GraphIntfc01. public int getNmbr(){ //Return number of functions to process. Must // not exceed 5. return 4; }//end getNmbr //-------------------------------------------// public double f1(double x){ int index = (int)Math.round(x); //This version of this method returns the // signal plus noise. if(index < 0 || index > noise.length-1){ return 0; }else{ //Scale for display and return. return noise[index] * 100.0; }//end else }//end f1 //-------------------------------------------// public double f2(double x){ //Return the signal pluse int index = (int)Math.round(x); if(index < 0 || index > signal.length-1){ return 0; }else{ //Scale for display and return. return signal[index] * 100.0; }//end else }//end f2 //-------------------------------------------// public double f3(double x){ //Return convolution output int index = (int)Math.round(x); if(index < 0 || index > output.length-1){ return 0; }else{ //Scale for display and return. return output[index] * 25.0; }//end else }//end f3 //-------------------------------------------// public double f4(double x){ //Return peakMarker int index = (int)Math.round(x); if(index < 0 || index > peakMarker.length-1){ return 0; }else{ //Scale for display and return. return peakMarker[index] * 1.0; }//end else }//end f4 //-------------------------------------------// public double f5(double x){ return 0.0; }//end f5 //-------------------------------------------// //This method generates a pulse with a linear // frequency sweep from zero to 3300 Hz at // a sampling rate of 16000 samples per // second. void fmSweep(int pulseLen,double[] output){ double twoPI = 2*Math.PI; double sampleRate = 16000.0; double lowFreq = 0.0; double highFreq = 3300.0; for(int cnt = 0; cnt < pulseLen; cnt++){ double time = cnt/sampleRate; double freq = lowFreq + cnt*(highFreq-lowFreq)/pulseLen; double sinValue = Math.sin(twoPI*freq*time); output[cnt] = sinValue; }//end for loop }//end method fmSweep }//end class Dsp040b //=============================================// //This class provides a static method named // convolve, which applies an incoming // convolution operator to an incoming set of // data and deposits the filtered data in an // output array whose reference is received as an // incoming parameter. class Convolve01{ public static void convolve(double[] data, double[] operator, double[] output){ //Apply the operator to the data, dealing // with the index reversal required by // convolution. int dataLen = data.length; int operatorLen = operator.length; for(int i = 0;i < dataLen-operatorLen;i++){ output[i] = 0; for(int j = operatorLen-1;j >= 0;j--){ output[i] += data[i+j]*operator[j]; }//end inner loop //Divide by the length of the operator }//end outer loop }//end convolve method }//end Class Convolve01 //=============================================// Listing 16 |
/* File Graph03.java Copyright 2002, R.G.Baldwin This program is very similar to Graph01 except that it has been modified to allow the user to manually resize and replot the frame. Note: This program requires access to the interface named GraphIntfc01. This is a plotting program. It is designed to access a class file, which implements GraphIntfc01, and to plot up to five functions defined in that class file. The plotting surface is divided into the required number of equally sized plotting areas, and one function is plotted on cartesian coordinates in each area. The methods corresponding to the functions are named f1, f2, f3, f4, and f5. The class containing the functions must also define a method named getNmbr(), which takes no parameters and returns the number of functions to be plotted. If this method returns a value greater than 5, a NoSuchMethodException will be thrown. Note that the constructor for the class that implements GraphIntfc01 must not require any parameters due to the use of the newInstance method of the Class class to instantiate an object of that class. If the number of functions is less than 5, then the absent method names must begin with f5 and work down toward f1. For example, if the number of functions is 3, then the program will expect to call methods named f1, f2, and f3. It is OK for the absent methods to be defined in the class. They simply won't be invoked. The plotting areas have alternating white and gray backgrounds to make them easy to separate visually. All curves are plotted in black. A cartesian coordinate system with axes, tic marks, and labels is drawn in red in each plotting area. The cartesian coordinate system in each plotting area has the same horizontal and vertical scale, as well as the same tic marks and labels on the axes. The labels displayed on the axes, correspond to the values of the extreme edges of the plotting area. The program also compiles a sample class named junk, which contains five methods and the method named getNmbr. This makes it easy to compile and test this program in a stand-alone mode. At runtime, the name of the class that implements the GraphIntfc01 interface must be provided as a command-line parameter. If this parameter is missing, the program instantiates an object from the internal class named junk and plots the data provided by that class. Thus, you can test the program by running it with no command-line parameter. This program provides the following text fields for user input, along with a button labeled Graph. This allows the user to adjust the parameters and replot the graph as many times with as many plotting scales as needed: xMin = minimum x-axis value xMax = maximum x-axis value yMin = minimum y-axis value yMax = maximum y-axis value xTicInt = tic interval on x-axis yTicInt = tic interval on y-axis xCalcInc = calculation interval The user can modify any of these parameters and then click the Graph button to cause the five functions to be re-plotted according to the new parameters. Whenever the Graph button is clicked, the event handler instantiates a new object of the class that implements the GraphIntfc01 interface. Depending on the nature of that class, this may be redundant in some cases. However, it is useful in those cases where it is necessary to refresh the values of instance variables defined in the class (such as a counter, for example). Tested using JDK 1.4.0 under Win 2000. This program uses constants that were first defined in the Color class of v1.4.0. Therefore, the program requires v1.4.0 or later to compile and run correctly. **************************************/ import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import javax.swing.*; import javax.swing.border.*; class Graph03{ public static void main( String[] args) throws NoSuchMethodException, ClassNotFoundException, InstantiationException, IllegalAccessException{ if(args.length == 1){ //pass command-line paramater new GUI(args[0]); }else{ //no command-line parameter given new GUI(null); }//end else }// end main }//end class Graph03 definition //===================================// class GUI extends JFrame implements ActionListener{ //Define plotting parameters and // their default values. double xMin = 0.0; double xMax = 400.0; double yMin = -100.0; double yMax = 100.0; //Tic mark intervals double xTicInt = 20.0; double yTicInt = 20.0; //Tic mark lengths. If too small // on x-axis, a default value is // used later. double xTicLen = (yMax-yMin)/50; double yTicLen = (xMax-xMin)/50; //Calculation interval along x-axis double xCalcInc = 1.0; //Text fields for plotting parameters JTextField xMinTxt = new JTextField("" + xMin); JTextField xMaxTxt = new JTextField("" + xMax); JTextField yMinTxt = new JTextField("" + yMin); JTextField yMaxTxt = new JTextField("" + yMax); JTextField xTicIntTxt = new JTextField("" + xTicInt); JTextField yTicIntTxt = new JTextField("" + yTicInt); JTextField xCalcIncTxt = new JTextField("" + xCalcInc); //Panels to contain a label and a // text field JPanel pan0 = new JPanel(); JPanel pan1 = new JPanel(); JPanel pan2 = new JPanel(); JPanel pan3 = new JPanel(); JPanel pan4 = new JPanel(); JPanel pan5 = new JPanel(); JPanel pan6 = new JPanel(); //Misc instance variables int frmWidth = 408; int frmHeight = 430; int width; int height; int number; GraphIntfc01 data; String args = null; //Plots are drawn on the canvases // in this array. Canvas[] canvases; //Constructor GUI(String args)throws NoSuchMethodException, ClassNotFoundException, InstantiationException, IllegalAccessException{ if(args != null){ //Save for use later in the // ActionEvent handler this.args = args; //Instantiate an object of the // target class using the String // name of the class. data = (GraphIntfc01) Class.forName(args). newInstance(); }else{ //Instantiate an object of the // test class named junk. data = new junk(); }//end else //Create array to hold correct // number of Canvas objects. canvases = new Canvas[data.getNmbr()]; //Throw exception if number of // functions is greater than 5. number = data.getNmbr(); if(number > 5){ throw new NoSuchMethodException( "Too many functions. " + "Only 5 allowed."); }//end if //Create the control panel and // give it a border for cosmetics. JPanel ctlPnl = new JPanel(); ctlPnl.setLayout(//?rows x 4 cols new GridLayout(0,4)); ctlPnl.setBorder( new EtchedBorder()); //Button for replotting the graph JButton graphBtn = new JButton("Graph"); graphBtn.addActionListener(this); //Populate each panel with a label // and a text field. Will place // these panels in a grid on the // control panel later. pan0.add(new JLabel("xMin")); pan0.add(xMinTxt); pan1.add(new JLabel("xMax")); pan1.add(xMaxTxt); pan2.add(new JLabel("yMin")); pan2.add(yMinTxt); pan3.add(new JLabel("yMax")); pan3.add(yMaxTxt); pan4.add(new JLabel("xTicInt")); pan4.add(xTicIntTxt); pan5.add(new JLabel("yTicInt")); pan5.add(yTicIntTxt); pan6.add(new JLabel("xCalcInc")); pan6.add(xCalcIncTxt); //Add the populated panels and the // button to the control panel with // a grid layout. ctlPnl.add(pan0); ctlPnl.add(pan1); ctlPnl.add(pan2); ctlPnl.add(pan3); ctlPnl.add(pan4); ctlPnl.add(pan5); ctlPnl.add(pan6); ctlPnl.add(graphBtn); //Create a panel to contain the // Canvas objects. They will be // displayed in a one-column grid. JPanel canvasPanel = new JPanel(); canvasPanel.setLayout(//?rows,1 col new GridLayout(0,1)); //Create a custom Canvas object for // each function to be plotted and // add them to the one-column grid. // Make background colors alternate // between white and gray. for(int cnt = 0; cnt < number; cnt++){ switch(cnt){ case 0 : canvases[cnt] = new MyCanvas(cnt); canvases[cnt].setBackground( Color.WHITE); break; case 1 : canvases[cnt] = new MyCanvas(cnt); canvases[cnt].setBackground( Color.LIGHT_GRAY); break; case 2 : canvases[cnt] = new MyCanvas(cnt); canvases[cnt].setBackground( Color.WHITE); break; case 3 : canvases[cnt] = new MyCanvas(cnt); canvases[cnt].setBackground( Color.LIGHT_GRAY); break; case 4 : canvases[cnt] = new MyCanvas(cnt); canvases[cnt]. setBackground(Color.WHITE); }//end switch //Add the object to the grid. canvasPanel.add(canvases[cnt]); }//end for loop //Add the sub-assemblies to the // frame. Set its location, size, // and title, and make it visible. getContentPane(). add(ctlPnl,"South"); getContentPane(). add(canvasPanel,"Center"); setBounds(0,0,frmWidth,frmHeight); if(args == null){ setTitle("Graph03, " + "Copyright 2002, " + "Richard G. Baldwin"); }else{ setTitle("Graph03/" + args + " Copyright 2002, " + "R. G. Baldwin"); }//end else setVisible(true); //Set to exit on X-button click setDefaultCloseOperation( EXIT_ON_CLOSE); //Guarantee a repaint on startup. for(int cnt = 0; cnt < number; cnt++){ canvases[cnt].repaint(); }//end for loop }//end constructor //---------------------------------// //This event handler is registered // on the JButton to cause the // functions to be replotted. public void actionPerformed( ActionEvent evt){ //Re-instantiate the object that // provides the data try{ if(args != null){ data = (GraphIntfc01)Class. forName(args).newInstance(); }else{ data = new junk(); }//end else }catch(Exception e){ //Known to be safe at this point. // Otherwise would have aborted // earlier. }//end catch //Set plotting parameters using // data from the text fields. xMin = Double.parseDouble( xMinTxt.getText()); xMax = Double.parseDouble( xMaxTxt.getText()); yMin = Double.parseDouble( yMinTxt.getText()); yMax = Double.parseDouble( yMaxTxt.getText()); xTicInt = Double.parseDouble( xTicIntTxt.getText()); yTicInt = Double.parseDouble( yTicIntTxt.getText()); xCalcInc = Double.parseDouble( xCalcIncTxt.getText()); //Calculate new values for the // length of the tic marks on the // axes. If too small on x-axis, // a default value is used later. xTicLen = (yMax-yMin)/50; yTicLen = (xMax-xMin)/50; //Repaint the plotting areas for(int cnt = 0; cnt < number; cnt++){ canvases[cnt].repaint(); }//end for loop }//end actionPerformed //---------------------------------// //This is an inner class, which is used // to override the paint method on the // plotting surface. class MyCanvas extends Canvas{ int cnt;//object number //Factors to convert from double // values to integer pixel locations. double xScale; double yScale; MyCanvas(int cnt){//save obj number this.cnt = cnt; }//end constructor //Override the paint method public void paint(Graphics g){ //Get and save the size of the // plotting surface width = canvases[0].getWidth(); height = canvases[0].getHeight(); //Calculate the scale factors xScale = width/(xMax-xMin); yScale = height/(yMax-yMin); //Set the origin based on the // minimum values in x and y g.translate((int)((0-xMin)*xScale), (int)((0-yMin)*yScale)); drawAxes(g);//Draw the axes g.setColor(Color.BLACK); //Get initial data values double xVal = xMin; int oldX = getTheX(xVal); int oldY = 0; //Use the Canvas obj number to // determine which method to // invoke to get the value for y. switch(cnt){ case 0 : oldY = getTheY(data.f1(xVal)); break; case 1 : oldY = getTheY(data.f2(xVal)); break; case 2 : oldY = getTheY(data.f3(xVal)); break; case 3 : oldY = getTheY(data.f4(xVal)); break; case 4 : oldY = getTheY(data.f5(xVal)); }//end switch //Now loop and plot the points while(xVal < xMax){ int yVal = 0; //Get next data value. Use the // Canvas obj number to // determine which method to // invoke to get the value for y. switch(cnt){ case 0 : yVal = getTheY(data.f1(xVal)); break; case 1 : yVal = getTheY(data.f2(xVal)); break; case 2 : yVal = getTheY(data.f3(xVal)); break; case 3 : yVal = getTheY(data.f4(xVal)); break; case 4 : yVal = getTheY(data.f5(xVal)); }//end switch1 //Convert the x-value to an int // and draw the next line segment int x = getTheX(xVal); g.drawLine(oldX,oldY,x,yVal); //Increment along the x-axis xVal += xCalcInc; //Save end point to use as start // point for next line segment. oldX = x; oldY = yVal; }//end while loop }//end overridden paint method //---------------------------------// //Method to draw axes with tic marks // and labels in the color RED void drawAxes(Graphics g){ g.setColor(Color.RED); //Lable left x-axis and bottom // y-axis. These are the easy // ones. Separate the labels from // the ends of the tic marks by // two pixels. g.drawString("" + (int)xMin, getTheX(xMin), getTheY(xTicLen/2)-2); g.drawString("" + (int)yMin, getTheX(yTicLen/2)+2, getTheY(yMin)); //Label the right x-axis and the // top y-axis. These are the hard // ones because the position must // be adjusted by the font size and // the number of characters. //Get the width of the string for // right end of x-axis and the // height of the string for top of // y-axis //Create a string that is an // integer representation of the // label for the right end of the // x-axis. Then get a character // array that represents the // string. int xMaxInt = (int)xMax; String xMaxStr = "" + xMaxInt; char[] array = xMaxStr. toCharArray(); //Get a FontMetrics object that can // be used to get the size of the // string in pixels. FontMetrics fontMetrics = g.getFontMetrics(); //Get a bounding rectangle for the // string Rectangle2D r2d = fontMetrics.getStringBounds( array,0,array.length,g); //Get the width and the height of // the bounding rectangle. The // width is the width of the label // at the right end of the // x-axis. The height applies to // all the labels, but is needed // specifically for the label at // the top end of the y-axis. int labWidth = (int)(r2d.getWidth()); int labHeight = (int)(r2d.getHeight()); //Label the positive x-axis and the // positive y-axis using the width // and height from above to // position the labels. These // labels apply to the very ends of // the axes at the edge of the // plotting surface. g.drawString("" + (int)xMax, getTheX(xMax)-labWidth, getTheY(xTicLen/2)-2); g.drawString("" + (int)yMax, getTheX(yTicLen/2)+2, getTheY(yMax)+labHeight); //Draw the axes g.drawLine(getTheX(xMin), getTheY(0.0), getTheX(xMax), getTheY(0.0)); g.drawLine(getTheX(0.0), getTheY(yMin), getTheX(0.0), getTheY(yMax)); //Draw the tic marks on axes xTics(g); yTics(g); }//end drawAxes //---------------------------------// //Method to draw tic marks on x-axis void xTics(Graphics g){ double xDoub = 0; int x = 0; //Get the ends of the tic marks. int topEnd = getTheY(xTicLen/2); int bottomEnd = getTheY(-xTicLen/2); //If the vertical size of the // plotting area is small, the // calculated tic size may be too // small. In that case, set it to // 10 pixels. if(topEnd < 5){ topEnd = 5; bottomEnd = -5; }//end if //Loop and draw a series of short // lines to serve as tic marks. // Begin with the positive x-axis // moving to the right from zero. while(xDoub < xMax){ x = getTheX(xDoub); g.drawLine(x,topEnd,x,bottomEnd); xDoub += xTicInt; }//end while //Now do the negative x-axis moving // to the left from zero xDoub = 0; while(xDoub > xMin){ x = getTheX(xDoub); g.drawLine(x,topEnd,x,bottomEnd); xDoub -= xTicInt; }//end while }//end xTics //---------------------------------// //Method to draw tic marks on y-axis void yTics(Graphics g){ double yDoub = 0; int y = 0; int rightEnd = getTheX(yTicLen/2); int leftEnd = getTheX(-yTicLen/2); //Loop and draw a series of short // lines to serve as tic marks. // Begin with the positive y-axis // moving up from zero. while(yDoub < yMax){ y = getTheY(yDoub); g.drawLine(rightEnd,y,leftEnd,y); yDoub += yTicInt; }//end while //Now do the negative y-axis moving // down from zero. yDoub = 0; while(yDoub > yMin){ y = getTheY(yDoub); g.drawLine(rightEnd,y,leftEnd,y); yDoub -= yTicInt; }//end while }//end yTics //---------------------------------// //This method translates and scales // a double y value to plot properly // in the integer coordinate system. // In addition to scaling, it causes // the positive direction of the // y-axis to be from bottom to top. int getTheY(double y){ double yDoub = (yMax+yMin)-y; int yInt = (int)(yDoub*yScale); return yInt; }//end getTheY //---------------------------------// //This method scales a double x value // to plot properly in the integer // coordinate system. int getTheX(double x){ return (int)(x*xScale); }//end getTheX //---------------------------------// }//end inner class MyCanvas //===================================// }//end class GUI //===================================// //Sample test class. Required for // compilation and stand-alone // testing. class junk implements GraphIntfc01{ public int getNmbr(){ //Return number of functions to // process. Must not exceed 5. return 4; }//end getNmbr public double f1(double x){ return (x*x*x)/200.0; }//end f1 public double f2(double x){ return -(x*x*x)/200.0; }//end f2 public double f3(double x){ return (x*x)/200.0; }//end f3 public double f4(double x){ return 50*Math.cos(x/10.0); }//end f4 public double f5(double x){ return 100*Math.sin(x/20.0); }//end f5 }//end sample class junk Listing 17 |
/* File GraphIntfc01.java Copyright 2005, R.G.Baldwin Rev 5/14/04 This interface must be implemented by classes whose objects produce data to be plotted by programs such as Graph03 and Graph06. Tested using SDK 1.4.2 under WinXP. ************************************************/ public interface GraphIntfc01{ public int getNmbr(); public double f1(double x); public double f2(double x); public double f3(double x); public double f4(double x); public double f5(double x); }//end GraphIntfc01 Listing 18 |
Copyright 2005, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.
Richard has participated in numerous consulting projects, and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Programming Tutorials, which has gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro magazine.
In addition to his programming expertise, Richard has many years of practical experience in Digital Signal Processing (DSP). His first job after he earned his Bachelor's degree was doing DSP in the Seismic Research Department of Texas Instruments. (TI is still a world leader in DSP.) In the following years, he applied his programming and DSP expertise to other interesting areas including sonar and underwater acoustics.
Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.
-end-