Convolution and Matched Filtering in Java

Learn how to perform matched filtering in Java.  Learn how matched filtering often makes it possible to detect properly designed signals in noisy environments where simple frequency filtering alone cannot do the job.

Published:  March 22, 2005
By Richard G. Baldwin

Java Programming, Notes # 1488


Preface

The previous lesson entitled Convolution and Frequency Filtering in Java taught you how to use convolution to perform frequency filtering in Java.

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.

General Discussion

The purpose of this lesson is to help you understand the use of convolution for matched filtering. 

What is convolution?

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.)

Preview

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.

Sample Programs

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.

Running the program

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:

java Graph03 Dsp040b

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.

Run the Programs

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.

Summary

In this lesson, I have explained and illustrated the use of time-domain convolution for matched filtering.

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.

Complete Program Listings

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.

About the author

Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web.

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.

Baldwin@DickBaldwin.com

-end-