scripts/epochClasses/oddb/Synamps.java

/////////////////////////////////////////////////////////////////
// Standard epoch-definition script for ODDB data (customized for 
// Bg/Tg frequencies used for SynAmps recordings)
/////////////////////////////////////////////////////////////////
package oddb;

import java.io.*;
import java.text.*;
import java.util.*;

import epochClasses.*;
import generalClasses.*;
import recordingClasses.Recording;
import seriesClasses.*;
import static epochClasses.EpochSelector.*;

/////////////////////////////////////////////////////////////////
/** Standard epoch-definition script for ODDB data (customized for 
 * Bg/Tg frequencies used for SynAmps recordings).
 * This script generates a list of epoch times; and for each epoch
 * generates a standard set of attributes.  The attribute table is 
 * a flexible way for attaching all manner of information to each epoch.
 * This information may be used later when subaveraging epochs.
 *
 * <p>This script also defines the start and duration of epochs through
 * <ul><li><tt>defaultEpochStart</tt>: the offset of epoch beginnings from
 *     the times listed in the the (obligatory) attribute <tt>Time</tt>.
 *     A value of -0.2 or -0.3 is common for ERPs.</li>
 *     <li><tt>defaultEpochDur</tt>: the duration of epochs, in seconds.
 *     A value of 1.0 is common for ERPs.</li>
 * </ul>
 *
 * <p>Attribute values, and epoch start and duration, are reported 
 * to the calling program via the methods defined in the superclass
 * <tt>EpochScript</tt>
 */
public class Synamps extends EpochScript
{
    private static float defaultEpochStart = -0.2f;
    private static float defaultEpochDur = 1.0f;
    private float rt0 = 0.1f;           // minimum accpetable RT, in sec
    private float rt1 = 1.0f;           // maximum acceptable RT, in sec
    private String[][] th = null;       // column header information
    private Object[][] td = null;       // main results array
    private int iteratorEventN = 0;     // used only by iterator, next()
    private int nAttrib;                // number of attribute columns
    private ArrayList<Event> runtimeEvents = null;  // obtained from Recording

    ////////////////////////////////////////////////////////////////////
    /** This script generates a standard set of attributes:
     * <ul><li><tt>Time</tt>: points within the span of the recording.  In
     *     the case of ERP recordings, these times usually coincide with
     *     significant stimuli.  However they can be offset for stimuli, or
     *     else identify EDA onsets, K-complexes, hypoxic events, etc. <b>This
     *     attribute is obligatory.</b></li>
     *     <li><tt>Stim</tt>: a string label that is commonly used for
     *     identifying epochs.  It is here that an event (which contains 
     *     information like Tone, 1000 Hz) is assigned the label "Bg".</li>
     *     <li><tt>RT, RTclass</tt>: values that are derived from stimulus and 
     *     button press events, and incorporates some allowed range.  The 
     *     latter is a String value, which simplifies creation of subsets
     *     of epochs based on RT speed.</li>
     *     <li><tt>AlphaPow, AlphaPha, AlphaQ</tt>: values that quantify 
     *     the magnitude and phase of alpha.</li>
     *     <li><tt>ScalpSD, ScalpPP</tt>: maximal SD and peak-to-peak range
     *     within each epoch.  Only scalp channels are considered.</li>
     *     <li>And so on …</li>
     * </ul>
     * (Check code for a definitive list.)  It also sets the epoch start
     * offset and epoch duration.
     * @param rec Reference to the data. This is used to retrieve both time 
     *        series and event information.
     */
    public Synamps(Recording rec) {
        super(rec, defaultEpochStart, defaultEpochDur);

        // Time series are available via the inherited method getSeries()
        // Events are available via the inherited method getEvents()
        // (Equally, they could be obtained directly from rec.)
        runtimeEvents = getEvents();

        // Generate meta information about attribute table
        String[] names = {
            "Time", "Stim", "StimResp", "RT", "RTclass",
            "AlphaPow", "AlphaPha", "AlphaQ", 
            "ScalpSD", "ScalpPP",
            "Seq", "BgP","TgP", "PN"};
        String[] types = {
            "FLOAT", "VARCHAR5", "VARCHAR10", "FLOAT", "VARCHAR6",
            "FLOAT", "FLOAT", "VARCHAR3",
            "FLOAT", "FLOAT",
            "VARCHAR6", "VARCHAR5", "VARCHAR5", "VARCHAR5"};
        assert names.length==types.length: "Synamps.java: length mismatch";
        nAttrib = names.length;
        th = new String[2][nAttrib];
        for(int i=0; i<nAttrib; i++) {
            th[0][i] = names[i];
            th[1][i] = types[i];
        }

        // Allocate attribute table
        if(runtimeEvents==null || runtimeEvents.size()==0) return;
        td = new Object[runtimeEvents.size()][nAttrib];

        // Initialize attribute table, one (or a few) columns at a time
        updateTime(0);
        updateStim(1);
        updateRT(2,3,4);
        updateSpectralPowerAtStim(5,6,7);
        updateArtif(8,9); 
        updateSeq(1, 10);
        updateP(1, 11,12,13);
    } // Synamps

    ////////////////////////////////////////////////////////////////////
    /** Dump summary of this object, to be used for diagnostics
     * @return String representation of this object
     */
    @Override
    public String toString() {
        String s = super.toString();      // get superclass description
        s += "Allowed range of reaction times: "+rt0+".."+rt1;
        return s;
    } // toString

    ////////////////////////////////////////////////////////////////////
    /** This returns the name and datatype of any number of attributes.
     * Each name and datatype characterizes one column of the table
     * of attributes.
     * @return The name and datatype for each of <tt>nCols</tt> columns
     *         of the attribute table.  The array is dimensioned [2][nCol].
     */
    @Override
    public String[][] getAttribNameType() {
        return th;
    } // getAttribNameType

    ////////////////////////////////////////////////////////////////////
    /** The attribute table is returned one row at a time.  This checks 
     * if there are more rows available.
     */
    @Override
    public boolean hasNext() {
        return td!=null && iteratorEventN < td.length;
    } // hasNext

    ////////////////////////////////////////////////////////////////////
    /** Returns the next row of attribute table.  If there are no more
     * rows remaining, then <tt>null</tt> is returned.  Note that is 
     * possible that each element in the returned array is itself
     * <tt>null</tt>, so calling code should be prepared to deal with
     * such cases too.
     */
    @Override
    public Object[] next() {
        return (td!=null && iteratorEventN<runtimeEvents.size())?
            td[iteratorEventN++]: null;
    } // next


    ////////////////////////////////////////////////////////////////////
    //                   private methods                              //
    ////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][colTime] with the event times, in seconds.
     * NOTE: this is obligatory, unlike all other array columns.
     */
    private void updateTime(int colTime) {
        for(int row=0; row<runtimeEvents.size(); row++) {
            Event e = runtimeEvents.get(row);
            td[row][colTime] = new Float((float)e.getTime());
        }
    } // updateTime

    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][colStim].
     * 
     * This version is specific to oddball paradigms where 500Hz is a 'Bg'
     * and 1000Hz is a 'Tg', and button responses are expected to targets.
     */
    private void updateStim(int colStim) {
        for(int row=0; row<runtimeEvents.size(); row++) {
            Event e = runtimeEvents.get(row);

            if(e.getDesc().matches("^Tone:1000Hz.*")) {
                td[row][colStim] = "Bg";
            } else if(e.getDesc().matches("^Tone:1500Hz.*")) {
                td[row][colStim] = "Tg";
            } else {
                td[row][colStim] = null;
            }
        }
    } // updateStim

    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][colStimResp], td[*][colRT] and td[*][colRTCat], 
     * which refer to the combination of stimulus type with response, 
     * reaction time (in sec) and a categorical version of this.
     * 
     * This version is specific to oddball paradigms where 1000Hz is a 'Bg'
     * and 1500Hz is a 'Tg', and button responses are expected to targets.
     * It creates a categorical variable indicating the speed of response.
     * All responses (valid and invalid) are included, when calculating the 
     * cutoff between categories.
     */
    private void updateRT(int colStimResp, int colRT, int colRTCat) {
        // Initialize rtArray[] and td[][colStimResp]
        int nEvents = runtimeEvents.size();
        float[] rtArray = new float[nEvents];
        for(int row=0; row<nEvents; row++) {
            Event e = runtimeEvents.get(row);
            rtArray[row] = offsetToDesc(runtimeEvents,row,
                                        "^Butt:[RL].*",rt0,rt1);
            if(e.getDesc().matches("^Tone:1000Hz.*")) {
                td[row][colStimResp] = (Float.isNaN(rtArray[row]))?
                    "BgNoresp": "BgResp";
            } else if(e.getDesc().matches("^Tone:1500Hz.*")) {
                td[row][colStimResp] = (Float.isNaN(rtArray[row]))?
                    "TgNoresp": "TgResp";
            } else {
                td[row][colStimResp] = null;
            }
        }

        // Calculate mean
        float rtMean = 0;
        int rtMeanN = 0;
        for(int row=0; row<nEvents; row++) {
            if(!Float.isNaN(rtArray[row])) {
                rtMean += rtArray[row];
                rtMeanN++;
            }
        }
        rtMean = (rtMeanN==0)? Float.NaN: rtMean/rtMeanN;

        for(int row=0; row<nEvents; row++) {
            if(td[row][colStimResp] != null) {
                // This is a stimulus event: update RT
                if(Float.isNaN(rtArray[row]))
                    td[row][colRT] = null;
                else
                    td[row][colRT] = new Float(rtArray[row]);

                // Update categorized RT
                if(Float.isNaN(rtArray[row])) td[row][colRTCat] = "NoRT";
                else if(rtArray[row]<rtMean)  td[row][colRTCat] = "FastRT";
                else                          td[row][colRTCat] = "SlowRT";
            } else {
                // This event is not a stimulus event
                td[row][colRT] = null;
                td[row][colRTCat] = null;
            }
        }
    } // updateRT

    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][colPow] td[*][colPha] and td[*][colPhaseBand],
     * which refer to spectral band power, phase, and a categorical version
     * of phase.  Alpha is quantified for some brief (e.g. 1-sec) period 
     * following the times listed in <tt>float[] times</tt>.
     *
     * This version is specific to 'Pz' and to the alpha band.
     */
    private void updateSpectralPowerAtStim(int colPow, int colPha,
                                           int colPhaseBand) {
        // Which site to look at
        String site = "Pz";
        // Definition of spectral band: binWidth * binN = 10.0 Hz => alpha
        float binWidth = 1.0f;
        int binN = 10;

        // Estimate auxiliary values
        int nEvents = runtimeEvents.size();
        float[] times = new float[nEvents];
        for(int row=0; row<nEvents; row++) {
            Event e = runtimeEvents.get(row);
            times[row] = (float)e.getTime();
            //System.out.print(times[row]+" ");  // DEBUG
        }
        // Calculate alpha power and phase at Pz
        ArrayList ss = getSeries();
        int chanN = 0;
        for(chanN=0; chanN<ss.size(); chanN++) {
            Series s = (Series)(ss.get(chanN));
            if(s.getPrimaryLabel().equalsIgnoreCase(site)) break;
        }
        Float[][] alpha = new Float[nEvents][2];
        if(chanN<ss.size())
            getEegAtChan((Series)(ss.get(chanN)), binWidth, binN, times, alpha);

        // Update power, phase and categorized phase
        for(int row=0; row<nEvents; row++) {
            td[row][colPow] = alpha[row][0];   // power
            td[row][colPha] = alpha[row][1];   // phase 
            if(alpha[row][1] != null) {
                int cat = Math.round(alpha[row][1]/180); if(cat<0) cat+=2;
                td[row][colPhaseBand] = (cat==1)? "Out": "In";
            } else
                td[row][colPhaseBand] = null;
        }
    } // updateSpectralPowerAtStim

    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][colScalpSd] and td[*][colScalpPP], which refer
     * to the maximal standard deviation and peak-to-peak values, considering
     * all scalp signals.  These values are suitable for threshold-based
     * epoch rejection.
     */
    private void updateArtif(int colScalpSd, int colScalpPP) {
        ArrayList ss = getSeries();

        for(int row=0; row<runtimeEvents.size(); row++) {
            Event e = runtimeEvents.get(row);
            // Calculate SD and P-P measures by finding max in EEG chans only
            float maxSd = 0;
            float maxPP = 0;
            float tStart = (float)e.getTime()+getEpochStart();
            for(int chanN=0; chanN<ss.size(); chanN++) {
                if(ss.get(chanN) instanceof SeriesAnalog) {
                    SeriesAnalog s = (SeriesAnalog)(ss.get(chanN));
                    if(s.getMode()==DataMode.EEG && 
                       s.indexOf(tStart) >= 0 &&
                       s.indexOf(tStart+getEpochDur()) <= s.getNIndexes()) {
                        BasicStats bs = s.segment(tStart,getEpochDur(),
                                                  getEpochStart())
                            .getStats();
                        if(bs.sd > maxSd) maxSd = bs.sd;
                        if(bs.max-bs.min > maxPP) maxPP = bs.max-bs.min;
                    }
                }
            }

            // Update SD and P-P measures
            td[row][colScalpSd] = maxSd;
            td[row][colScalpPP] = maxPP;
        }
    } // updateArtif

    ////////////////////////////////////////////////////////////////////
    /** Label stimulus events according to preceeding sequence of stimuli.
     * The methods below classify each event according to its preceeding 
     * sequence.  It requires an exact match, and allows multiple matches: 
     * to resolve any ambiguities it chooses the _longest_ of these matches.
     */
    private String[] patternLabels = null;
    private LinkedList[] patterns = null;

    private void loadPatterns() {
        // A collection can be empty: such a pattern will always match
        Collection<String> firstB = Arrays.asList(new String[] {"^","Bg"});
        Collection<String> firstT = Arrays.asList(new String[] {"^","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t1t = Arrays.asList(new String[] {"Tg","Bg","Tg"});
        Collection<String> t2t = Arrays.asList(new String[] {"Tg","Bg","Bg","Tg"});
        Collection<String> t3t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Tg"});
        Collection<String> t4t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t5t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t6t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t7t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t8t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t9t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t10t = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Tg"});
        Collection<String> t1b = Arrays.asList(new String[] {"Tg","Bg"});
        Collection<String> t2b = Arrays.asList(new String[] {"Tg","Bg","Bg"});
        Collection<String> t3b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg"});
        Collection<String> t4b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg"});
        Collection<String> t5b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg"});
        Collection<String> t6b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg"});
        Collection<String> t7b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg"});
        Collection<String> t8b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg"});
        Collection<String> t9b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg"});
        Collection<String> t10b = Arrays.asList(new String[] {"Tg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg","Bg"});

        // A given label can be linked to more than one LinkedLists
        patternLabels = new String[] {"firstB","firstT",
                                      "T1BT", "T2BT", "T34BT", "T34BT", "T59BT",
                                      "T59BT", "T59BT", "T59BT", "T59BT", "T10BT",
                                      "T1B", "T2B", "T34B", "T34B", "T59B",
                                      "T59B", "T59B", "T59B", "T59B", "T10B"};
        patterns = new LinkedList[] {new LinkedList<String>(firstB),
                                     new LinkedList<String>(firstT),
                                     new LinkedList<String>(t1t),
                                     new LinkedList<String>(t2t),
                                     new LinkedList<String>(t3t),
                                     new LinkedList<String>(t4t),
                                     new LinkedList<String>(t5t),
                                     new LinkedList<String>(t6t),
                                     new LinkedList<String>(t7t),
                                     new LinkedList<String>(t8t),
                                     new LinkedList<String>(t9t),
                                     new LinkedList<String>(t10t),
                                     new LinkedList<String>(t1b),
                                     new LinkedList<String>(t2b),
                                     new LinkedList<String>(t3b),
                                     new LinkedList<String>(t4b),
                                     new LinkedList<String>(t5b),
                                     new LinkedList<String>(t6b),
                                     new LinkedList<String>(t7b),
                                     new LinkedList<String>(t8b),
                                     new LinkedList<String>(t9b),
                                     new LinkedList<String>(t10b)};
        if(patternLabels.length != patterns.length) {
            System.out.println("Fatal error in the scripted method loadPatterns()");
            System.out.println("Mismatching arrays defined by loadPatterns()");
            System.exit(2);
        }
    } // loadPatterns


    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][colSeq], which refers to a summary of the preceding
     * sequence of stimuli.
     */
    private void updateSeq(int colStim, int colSeq) {
        loadPatterns();

        // Make sure th[1][colStim] specified a VARCHAR
        if(!th[1][colStim].startsWith("VARCHAR")) {
            System.out.println("Fatal error in Synamps.java, updateSeq()");
            System.out.println("The argument 'colStim' should identify an "+
                               "attribute of type VARCHAR; but column"+
                               colStim+"=\""+th[1][colStim]+"\"");
            System.exit(2);
        }

        // Make sure th[1][colSeq] specified a VARCHAR wide enough for labels
        int maxLength = 0;
        for(int i=0; i<patternLabels.length; i++)
            if(patternLabels[i].length() > maxLength)
                maxLength = patternLabels[i].length();
        if("other".length() > maxLength)
            maxLength = "other".length();
        int available = Integer.parseInt(th[1][colSeq]
                                         .substring("VARCHAR".length()) );
        if(maxLength>available) {
            System.out.println("Fatal error in sequence.bsh, updateSeq()");
            System.out.println("Labels in loadPatterns() require th[1]["+
                               colSeq+"]=\"VARCHAR"+maxLength+"\"");
            System.exit(2);
        }

        for(int row=0; row<runtimeEvents.size(); row++) {
            // Try each pattern in turn
            int bestMatch = -1;
            for(int patN=0; patN<patterns.length; patN++) {
                LinkedList pattern = patterns[patN];
        
                boolean match = true;
                Iterator iter = pattern.descendingIterator();
                for(int ev=row; ev>=0 && match && iter.hasNext(); ev--) {
                    if(td[ev][colStim]==null) continue;
                    String p = (String)iter.next();
                    if(((String)td[ev][colStim]).equalsIgnoreCase(p)) {
                        ;    // event and pattern still match: do nothing
                    } else {
                        match = false;  // mismatch
                    }
                }
        
                // Was there really a match?
                if(match && !iter.hasNext()) {
                    // pattern was fully matched
                    if(bestMatch<0) {
                        // update index (unconditionally)
                        bestMatch = patN;
                    } else {
                        // update index (if current pattern is longer 
                        // than the previous matching pattern)
                        int thisPatternSize = pattern.size();
                        if(((String)pattern.getFirst()).equals("^"))
                            thisPatternSize--;
                        int bestPatternSize = patterns[bestMatch].size();
                        if(((String)patterns[bestMatch].getFirst()).equals("^"))
                            bestPatternSize--;
                        if(thisPatternSize >  bestPatternSize) 
                            bestMatch = patN;
                    }
                    // DEBUG
                    //System.out.println("Event "+row+": pattern="+pattern+
                    //                   " label="+patternLabels[bestMatch]);
                } else if(match && iter.hasNext() &&
                          ((String)iter.next()).equals("^")) {
                    // pattern was fully matched including '^": update index
                    bestMatch = patN;
                    // DEBUG
                    //System.out.println("Event "+row+": pattern="+pattern+
                    //                   " label="+patternLabels[bestMatch]);
                } else {
                    ;  // either mismatch or partial match
                }
            }

            if(bestMatch==-1)
                td[row][colSeq] = "other";
            else
                td[row][colSeq] = patternLabels[bestMatch];
            // DEBUG
            //System.out.println("Event "+row+":  label="+td[row][colSeq]);
        }
    } // updateSeq


    ////////////////////////////////////////////////////////////////////
    /** Initializes td[*][bgPCol], td[*][tgPCol], td[*][pCol], which contain
     * labels that suggest, for each epoch, the fractional point within the
     * paradigm.  Since <tt>nGroups=10</tt> the labels reflect <i>deciles</i>,
     * and does so independently for backgrounds and targets.
     * <p>Specifically the column 'BgP' contains values 'P0', 'P1',...,'P9'
     * depending on whether the Bg is in the first 28, second 28, etc, out
     * of the total 280 backgrounds.  Similarly, the 60 targets have the same
     * labels, 6 of each.
     */
    private void updateP(int colStim, int bgPCol, int tgPCol, int pCol) {
        // Make sure th[1][colStim] specified a VARCHAR
        if(!th[1][colStim].startsWith("VARCHAR")) {
            System.out.println("Fatal error in Synamps.java, updateSeq()");
            System.out.println("The argument 'colStim' should identify an "+
                               "attribute of type VARCHAR; but column"+
                               colStim+"=\""+th[1][colStim]+"\"");
            System.exit(2);
        }

        // nGroups equals how many groups the Bgs and Tgs are to be divided into
        int nGroups = 10;

        // Count number of Bg and Tg stimuli
        int nBg = 0;
        int nTg = 0;
        for(int row=0; row<runtimeEvents.size(); row++) {
            if(td[row][colStim]!=null) {
                if(((String)td[row][colStim]).equalsIgnoreCase("Bg")) nBg++;
                if(((String)td[row][colStim]).equalsIgnoreCase("Tg")) nTg++;
            }
        }
    
        for(int row=0; row<runtimeEvents.size(); row++) {
            td[row][pCol] = null;

            if(td[row][colStim]!=null &&
               ((String)td[row][colStim]).equalsIgnoreCase("Bg")) {
                int count = 0;
                for(int i=0; i<row; i++)
                    if(td[i][colStim]!=null &&
                       ((String)td[i][colStim]).equalsIgnoreCase("Bg")) count++;
                td[row][bgPCol] = "P"+Integer.toString(count*nGroups/nBg);
                td[row][pCol] = td[row][bgPCol];
            } else
                td[row][bgPCol] = null;
    
            if(td[row][colStim]!=null &&
               ((String)td[row][colStim]).equalsIgnoreCase("Tg")) {
                int count = 0;
                for(int i=0; i<row; i++)
                    if(td[i][colStim]!=null &&
                       ((String)td[i][colStim]).equalsIgnoreCase("Tg")) count++;
                td[row][tgPCol] = "P"+Integer.toString(count*nGroups/nTg);
                td[row][pCol] = td[row][tgPCol];
            } else
                td[row][tgPCol] = null;
        }
    } // updateP

}

 


Validate HTML CSS Generated 2011-08-12T10:32:18+1000 Chris Rennie