Package edu.iris.dmc.qc.controlet

The controlet package is exposed to Quality Control Applications so that they may interact with the Quack program.

See:
          Description

Interface Summary
QcConfig Gives controlets access to their context and their initialization parameters.
QcContext Provides interfaces that allow QcControlets to interact with their container.
QcMetaData Encapsulates seismic waveform meta data.
QcRequest Encapsulates a measurement request.
QcResponse Provides methods for recording QC measurements.
QcWaveform QcWaveform encapsulates a seismic waveform signal from one channel for the time interval given by the request object passed to a controlet in its measureQuality() method.
 

Class Summary
QcControlet The abstract base class for all classes which measure waveform quality.
 

Exception Summary
QcException Thrown by controlets when the unexpected happens.
 

Package edu.iris.dmc.qc.controlet Description

The controlet package is exposed to Quality Control Applications so that they may interact with the Quack program.

Contents

Introduction

[Up]

Definition: a controlet is a class that subclasses QcControlet. Controlets must, at a minimum, implement the measureQuality() method. Quack calls the measureQuality() method when it needs to have a controlet measure the quality of seismic waveform data.

Definition: a Quality Control Application - QCA is a jar file containing one or more controlets, their support classes and a meta-data file named qcapp.xml. The information contained in qcapp.xml exposes the controlet classes to Quack (simply extending QcControlet is not enough to expose a controlet to Quack). The Quack program can load multiple QCAs, each of which may contain multiple controlets. Each QCA is loaded by a seperate class loader. This allows Quack to dynamically load and unload QCAs at runtime and it also prevents class naming conflicts between QCAs.

Architecture
Quack is patterned after architectures such as Java Web Server and Enterprise Application Server.

Java Web Server/Quack analogies:

Java Web Server
Quack
WAR file QCA file
Servlet Controlet
web.xml qcapp.xml

At regular time intervals (for example once an hour), Quack invokes the measureQuality() method of configured controlets. In the measureQuality() method, controlets are passed request and response objects. The request object contains information about which seismic channels need to be sampled, over what time interval, and where the data can be found along with meta information such as network/station/location/channel names, and instrument responses.

The response object allows a controlet to record zero or more values for each measurement. For example, a single controlet could record the values of both mean and rms.

The response object has two methods:

The first method allows a measurement to be associated with an individual waveform, while the second method allows a measurement to be associated with multiple waveforms.

Note:Please do not confuse the response object with the seismic instrument response which maybe retrieved from QcWaveform.getRespData().

Steps to write a Quack Quality Control Application

[Up]

  1. Write one or more classes that subclass QcControlet
  2. If needed, write support classes for your contontrolets
  3. Write a file named qcapp.xml. This file is read by Quack to determine what classes are exposed controlets, and to make associations between simple names (e.g "RMS") and class names (e.g. "edu.whatsamatau.geophys.RmsControlet").
  4. Compile source files and jar the resulting class files along with the qcapp.xml file which must be in a jar subdirectory named xml

Sample QCA jar file

[Up]

		jar -tf sample_qca.jar 
		META-INF/
		META-INF/MANIFEST.MF
		edu/
		edu/whatsamatau/
		edu/whatsamatau/geophys/
		edu/whatsamatau/geophys/util/
		edu/whatsamatau/geophys/RmsControlet.class
		edu/whatsamatau/geophys/util/GMTDateFormater.class
		edu/whatsamatau/geophys/util/DataBuffer.class
		[More support classes....]
		xml/
		xml/qcapp.xml

Sample qcapp.xml file

[Up]

    <qc-app>
        <qc-app-name>Sample QCA</qc-app-name>
        <qc-app-majorversion>1</qc-app-majorversion>
        <qc-app-minorversion>0</qc-app-minorversion>
        <qc-app-author>Rocket J Squirrel</qc-app-author>
        <qc-app-institution>Buena Vista</qc-app-institution>
            <controlet>
                <controlet-name>RMS</controlet-name>
                <controlet-class>edu.whatsamatau.geophys.RmsControlet</controlet-class>
                <controlet-type>1</controlet-type>
                <init-param>
                    <!-- custom initialization parameter used by RmsControlet -->
                    <!-- see the init() method in RmsControlet.java -->
                    <param-name>VerboseDebug</param-name>
                    <param-value>false</param-value>
                </init-param>
            </controlet>
    </qc-app>

Sample Controlet: RmsControlet.java

[Up]

package edu.whatsamatau.geophys;
 
import edu.iris.dmc.qc.controlet.*;
import java.util.*;
import java.net.*;
import java.io.*;
 
// DataBuffer is used for parsing miniSEED data.
// The controlet API does no parsing of miniSEED data.
// For maximum flexibility, this task is left to the controlet implementation.
import edu.whatsamatau.geophys.util.DataBuffer;
 
// used for verbose debug message logging
import edu.whatsamatau.geophys.util.GMTDateFormater;
 
/**
 * A controlet for calculating Mean and Variance of data.
 * Data is returned in the floats hashtable: 
 *
 * <table border="1">
 *
 *  <tr>  <td>Key</td> <td>Value                 </td> </tr>
 *  <tr>  <td>1  </td> <td>Number of samples (N) </td> </tr>
 *  <tr>  <td>2  </td> <td>Average               </td> </tr>
 *  <tr>  <td>3  </td> <td>Variance              </td> </tr>
 *
 * </table>
 *
 */
public class RmsControlet extends QcControlet {
 
    // set to true to log verbose debug messages
    private boolean verboseDebug = false;
 
    // ---------------------------------------------------
    // Optional method specified by base class QcControlet
    // This method is called once before the controlet goes
    // into service. 
    //
    // In this example we use init() to turn logging on/off. 
    // The config object is inherited from QcControlet.
    // We get VerboseDebug from the qcapp.xml file.
    // ...
    // <init-param>
    //     <param-name>VerboseDebug</param-name>
    //     <param-value>true</param-value>
    // </init-param>
    // ...
    // ---------------------------------------------------
    public void init() {
        String s = config.getInitParameter("VerboseDebug");
        if( s != null && s.equalsIgnoreCase("true") ) {
            verboseDebug = true;
            log("init(): verboseDebug enabled");
        } else {
            verboseDebug = false;
        }
    }
 
    // --------------------------------------------
    // This method must be implemented.
    // measureQuality is specified by the abstract 
    // base class QcControlet
    // --------------------------------------------
    public void measureQuality(QcRequest request, QcResponse response) throws QcException {
 
        // log a message indicating that we were called
        if( verboseDebug )
            log("RmsControlet.measureQuality()");
 
        // loop over all of the waveforms. There will be one waveform for each
        // channel that needs to be measured.
        //
        QcWaveform waveform[] = request.getWaveforms();
        for( int i = 0; i < waveform.length; i++ ) {
 
            //
            // Call the base classe's stopMeasurement() method to see if we should
            // stop. This allows the controlet to die gracefully.
            // This is especially useful in the developement process;
            // Quack cannot dynamically reload a QCA jar file if any of its
            // controlets are busy. 
            //
            // Depending on the controlet, you might want to
            // make this call at more than one point in your code.
            //
            if( this.stopMeasurement() ) {
                return;
            }
 
            if( verboseDebug ) {
                log( "working on waveform[" + i + "]");
            }
 
            try {
                //
                // given the waveform, and the start and end times of the sample
                // interval, estimate the avg and variance by calling our custom
                // private class computeAvgRMS...
                //
                float[] est = computeAvgRMS(waveform[i], request.getStartTime(), request.getEndTime());
 
                //
                // Results are stored in this format:
                //
                // est[0] = number of samples
                // est[1] = average
                // est[2] = variance
                //
                // est == null if not data was found
                //
 
                if( est == null ) {
                    continue;
                }
 
                if( verboseDebug ) {
                    log( "N/avg/RMS=" + est[0] + "/" + est[1] + "/" + est[2] );
                }
 
                // Only floatValues will be populated....
                Hashtable intValues     = null;
                Hashtable floatValues   = new Hashtable();
                Hashtable stringValues  = null;
                Hashtable booleanValues = null;
 
                floatValues.put( new Integer(0), new Float(est[0]) ); // the number of samples
                floatValues.put( new Integer(1), new Float(est[1]) ); // the avg
                floatValues.put( new Integer(2), new Float(est[2]) ); // the rms
 
                // pull the start and stop times out of the request
                Date startTime = request.getStartTime();
                Date stopTime  = request.getEndTime();
 
                // 
                // Have the response object store the results. This is where the
                // results are sent to the data base
                //
                try {
                    response.setValues( waveform[i], 
                                        intValues,     // empty
                                        floatValues,   // not empty
                                        stringValues,  // empty
                                        booleanValues, // empty
                                        startTime, 
                                        stopTime );
                } catch( IllegalArgumentException e ) {
                    log("unexpected exception:" +  e );
                    throw new QcException( "Unexpected IllegalArgumentException: " + e );
                }
            } catch( IOException e ) {
                log("unexpected exception:" +  e );
                throw new QcException( "Unexpected IOException: " + e );
            }
        }
    }
 
    //------------------------------------------------------------
    // compute the variance and Avg for the time range
    // Note: for simplicity all times are rounded to milliseconds.
    //       MiniSEED time resolution is 0.1 millisecond.
    //------------------------------------------------------------
    private float[] computeAvgRMS( 
                                    QcWaveform waveform, 
                                    Date startTime, 
                                    Date stopTime 
                                 ) 
        throws IOException, QcException {
 
        //-----------------------------------------------------
        //
        //            sum{ (xj - avg)^2 }
        // Variance = -------------------
        //                  (n - 1)
        //
        //            sum{ xj^2 } - 2*avg*sum{ xj } + n*avg^2
        //          = ----------------------------------------
        //                            n - 1
        //
        //   Let sumA = sum{ xj^2 }
        //   Let sumB = sum{ xj } --> avg = sumB/n
        //
        //            sumA - 2*sumB^2/n + n*sumB^2/(n*n)
        //          = ----------------------------------
        //                            n - 1
        //
        //            sumA - sumB^2/n
        //          = ---------------
        //                  n - 1
        //
        //   rms = squrt( variance )
        //
        //------------------------------------------------------
        double rms = 0.0;
        double avg = 0.0;
        int n = 0;
        double sumA = 0.0;
        double sumB = 0.0;
 
        //
        // the url array contains, in sequence, the urls of all of the miniseed
        // data that needs to be analyzed. The mini seed data is only for one
        // channel. Note: The url's may point to none-existent data! This can
        // happen if the url is generated, and then url file is deleted. 
        // A well written controlet should be able to handle this. 
        //
        URL url[] = waveform.getMiniSeedURLs();
 
        //
        // if the url array is empty, return null
        //
        if( url.length == 0 ) {
            log("no data to process, getMiniSeedURLs() returned empty array");
            return null;
        }
 
        //
        // The DataBuffer class is used to parse the miniseed data.
        //
        DataBuffer db = new DataBuffer(); // miniseed class
 
        //
        // Get the sample window in milliseconds since GMT 0:0:0 jan 1, 1970
        //
        long s1 = startTime.getTime(); // start of the sample interval
        long s2  = stopTime.getTime(); // end   of the sample interval
 
        //
        // Loop on all of the urls.
        //
        for( int i = 0; i < url.length; i++ ) {
 
            if( verboseDebug ) {
                log("working on url[" + i + "]=" + url[i]);
            }
 
            URLConnection uc;
            InputStream   in;
            byte          buffer[];
 
            //---------------------------------------------
            // get the record length and close the stream
            //---------------------------------------------
            uc = url[i].openConnection();
            uc.connect();
            in = uc.getInputStream();
            buffer = new byte[512];
 
            int readLength = in.read(buffer);
            if( readLength != buffer.length ) {
                log( "unable to read input stream, readLength=" + readLength);
                in.close();
                continue;
            }
            db.loadBuffer(buffer);
            int buffsize = db.getDataRecLen();
            in.close();
 
            //--------------------------------------------
            // Open the stream again and process
            //--------------------------------------------
            buffer = new byte[buffsize];
            uc = url[i].openConnection();
            uc.connect();
            in = uc.getInputStream();
 
            //--------------------------------------------------
            // MiniSEED data is made up of records. Each record
            // contains a variable number of sequential samples
            // loop over records.
            //--------------------------------------------------
            while( in.read(buffer) == buffer.length ) {
 
                db.loadBuffer(buffer);
 
                if( db.getDataRecLen() != buffer.length ) {
                    log("bad record length!");
                    throw new QcException("bad record length != " + buffer.length );
                }
 
                //
                // determine if the interval is within startTime and stopTime
                //
 
                //----------------------------------------------
                //  r1 - starttime of the record interval
                //       milliseconds since gmt 0:0:0 01/01/70
                //----------------------------------------------
                long r1 = db.getJavaDate().getTime(); 
 
                //---------------------------------------------
                // r2 - stoptime of the record interval
                //      milliseconds since gmt 0:0:0 01/01/70
                //
                //      Note the divide by 10. 
                //      This is to convert 0.1 msecs to msecs
                //---------------------------------------------
                double dRecLength = db.getRecInterval()/10.0;      
                long r2 = r1 + (long)dRecLength;      
 
                //-------------------------------------------------------
                // Determine if the record interval (r1,r2) is touched by
                // the sample interval (s1,s2)
                //-------------------------------------------------------
                boolean recordInSampleInterval = 
 
                    // start of the sample interval is in the record interval
                     ( r1 <= s1 && s1 <= r2 ) ||
 
                    // end of the sample interval is in the record interval
                     ( r1 <= s2 && s2 <= r2 ) ||
 
                    // sample interval surrounds the record interval
                     ( s1 <= r1 && r2 <= s2 ) ;
 

                if( verboseDebug ) {
                    // verbose debug message
                    // diagnoistic debug stuff
                    String header;
                    if( recordInSampleInterval ) {  
                        header = "\n-----record in sample interval------\n";
                    } else {
                        header = "\n-----record NOT in sample interval--\n";
                    }
 
                    String tmp = 
                        header + 
                        "s/c/n/l=" + 
                        db.getStn() + "/" + db.getChn() + "/" + db.getNet() + "/" + db.getLoc() + "\n" +
                        "   " + db.getYYYYDDD() + "\n" +
                        "   Date=    " + db.getDate() + "\n" +
                        "   JavaDate=" + GMTDateFormater.format(db.getJavaDate()) + "\n" +
                        "   samples=" + db.getNumSamples() +"\n" +
                        "   RecLen=" + db.getDataRecLen() +"\n" +
                        "   interval=" + db.getRecInterval() + "\n" +
                        "   rate=" + db.getSampleRate() + "\n" +
                        "   s1 = " + s1 + " " + GMTDateFormater.format(new Date(s1)) + "\n" +
                        "   s2 = " + s2 + " " + GMTDateFormater.format(new Date(s2)) + "\n" +
                        "   r1 = " + r1 + " " + GMTDateFormater.format(new Date(r1)) + "\n" +
                        "   r2 = " + r2 + " " + GMTDateFormater.format(new Date(r2));
                    log(tmp);
                }
 
                if( recordInSampleInterval ) {
 
                    //
                    // get the DataBuffer code to decode the miniseed record
                    //
                    int x[] = new int[db.getNumSamples()];
                    db.setDecoder(db.getBlk_1000().getDecodeString());
                    db.decodeBuffer();
                    db.packTrace(x);
 
                    float sampRate = db.getSampleRate();
 
                    //
                    // loop over the array of data. Only anlayze data that is
                    // within the sample interval (s1,s2)
                    //
                    for( int j = 0; j < x.length; j++ ) {
 
                        //
                        // rj - sample time
                        //
                        long rj = r1 + (long)(1000.0f * ((float)j)/sampRate);
 
                        if( s1 <= rj && rj <= s2 ) {
                            sumA += x[j]*x[j];
                            sumB += x[j];
                            n++;
                        }
 
                    } // end loop sample array
 
                } // end if in interval
 
            } // end while read
 
            in.close();
 
        } //end for url's
 
        avg = sumB/(double)n;
 
        //-----------------------------
        //            sumA - sumB^2/n
        // Variance = ---------------
        //                n - 1
        //-----------------------------
        double variance = (sumA - sumB*sumB/(double)n)/((double)n - 1.0);
 
        rms = Math.sqrt( variance );
        float ret[] = new float[3];
        ret[0] = (float)n;
        ret[1] = (float)avg;
        ret[2] = (float)rms;
        return ret;
    }
}

How to Access Meta Information

[Up]

Meta information is accessed through the QcWaveform interface. QcWaveform has two methods for accessing meta information getMetaData() and getRespData().

QcWaveform.getMetaData()

QcWaveform.getMetaData() returns an array of objects implementing the QcMetaData interface. QcMetaData encapsulates information about:

Each QcMetaData object returned by QcWaveform.getMetaData() has a start time and an end time. Normally, the array of QcMetaData objects returned by QcWaveform.getMetaData( ) will contain only one object because the time interval of the first array element will cover the entire time interval of the QcRequest object. However, if the meta data changes over the request's time interval, the array will contain more than one element. If the array contains zero elements, then no meta data is available for the seismic channel during the request's sample interval.

See:
QcWaveform.getMetaData()
QcMetaData.getStartTime() / QcMetaData.getEndTime()
QcRequest.getStartTime() / QcRequest.getEndTime()

QcWaveform.getRespData()

QcWaveform.getRespData() returns a String containing instrument response information in the form of a RESP file. This information will cover the request's sample time interval. The controlet is responsible for parsing this string. See QcWaveform.getRespData() for more details.