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
}