Print

Print


Author: [log in to unmask]
Date: Tue Mar  3 16:01:55 2015
New Revision: 2235

Log:
Updated trigger diagnostics. Basic GUI component added. Still under construction.

Added:
    java/trunk/users/src/main/java/org/hps/users/kmccarty/ClusterMatchStatus.java
    java/trunk/users/src/main/java/org/hps/users/kmccarty/DiagSnapshot.java   (with props)
    java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticGUIDriver.java   (with props)
    java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/
    java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/AbstractTablePanel.java   (with props)
    java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ClusterTablePanel.java   (with props)
    java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ComponentUtils.java   (with props)
    java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/DiagnosticUpdatable.java   (with props)
    java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/TableTextModel.java   (with props)
Modified:
    java/trunk/users/src/main/java/org/hps/users/kmccarty/PairTrigger.java
    java/trunk/users/src/main/java/org/hps/users/kmccarty/SinglesTrigger.java
    java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticDriver.java

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/ClusterMatchStatus.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/ClusterMatchStatus.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/ClusterMatchStatus.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,254 @@
+package org.hps.users.kmccarty;
+
+import java.awt.Point;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.hps.readout.ecal.triggerbank.SSPCluster;
+import org.lcsim.event.Cluster;
+
+public class ClusterMatchStatus {
+	// Track cluster statistics.
+	private int sspClusters   = 0;
+	private int reconClusters = 0;
+	private int matches       = 0;
+	private int failEnergy    = 0;
+	private int failPosition  = 0;
+	private int failHitCount  = 0;
+	
+	// Plot binning values.
+	private static final int TIME_BIN = 4;
+	private static final double ENERGY_BIN = 0.01;
+	private static final int TIME_BIN_HALF = TIME_BIN / 2;
+	private static final double ENERGY_BIN_HALF = ENERGY_BIN / 2;
+	
+	// Track plotting values for reconstructed and SSP clusters.
+	private Map<Integer, Integer> sspHitCountBins = new HashMap<Integer, Integer>();
+	private Map<Integer, Integer> reconHitCountBins = new HashMap<Integer, Integer>();
+	private Map<Point, Integer> sspPositionBins = new HashMap<Point, Integer>();
+	private Map<Point, Integer> reconPositionBins = new HashMap<Point, Integer>();
+	private Map<Integer, Integer> sspEnergyBins = new HashMap<Integer, Integer>();
+	private Map<Integer, Integer> reconEnergyBins = new HashMap<Integer, Integer>();
+	
+	// Track plotting values for cluster matching results.
+	private Map<Point, Integer> failPositionBins = new HashMap<Point, Integer>();
+	private Map<Point, Integer> allHenergyBins = new HashMap<Point, Integer>();
+	private Map<Point, Integer> failHenergyBins = new HashMap<Point, Integer>();
+	private Map<Integer, Integer> allTimeBins = new HashMap<Integer, Integer>();
+	private Map<Integer, Integer> failTimeBins = new HashMap<Integer, Integer>();
+	
+    public void addEvent(ClusterMatchEvent event, List<Cluster> reconClusters, List<SSPCluster> sspClusters) {
+    	// Update the number of reconstructed and SSP clusters
+		// that have been seen so far.
+		int sspCount = sspClusters == null ? 0 : sspClusters.size();
+		int reconCount = reconClusters == null ? 0 : reconClusters.size();
+		this.sspClusters   += sspCount;
+		this.reconClusters += reconCount;
+		
+		// Update the pair state information.
+		matches      += event.getMatches();
+		failEnergy   += event.getEnergyFailures();
+		failHitCount += event.getHitCountFailures();
+		failPosition += event.getPositionFailures();
+		
+		// In the special case that there are no SSP clusters, no pairs
+		// will be listed. All possible fails are known to have failed
+		// due to position.
+		if(sspClusters == null || sspClusters.isEmpty()) {
+			failPosition += (reconClusters == null ? 0 : reconClusters.size());
+		}
+    	
+    	// Update the plotting information for reconstructed clusters.
+		for(Cluster cluster : reconClusters) {
+			// Update the hit count bin data.
+			Integer hitCountCount = reconHitCountBins.get(cluster.getCalorimeterHits().size());
+			if(hitCountCount == null) { reconHitCountBins.put(cluster.getCalorimeterHits().size(), 1); }
+			else { reconHitCountBins.put(cluster.getCalorimeterHits().size(), hitCountCount + 1); }
+			
+			// Update the position bin data.
+			Point clusterPosition = TriggerDiagnosticUtil.getClusterPosition(cluster);
+			Integer positionCount = reconPositionBins.get(clusterPosition);
+			if(positionCount == null) { reconPositionBins.put(clusterPosition, 1); }
+			else { reconPositionBins.put(clusterPosition, positionCount + 1); }
+			
+			// Update the energy bin data.
+			int energyBin = (int) Math.floor(cluster.getEnergy() / ENERGY_BIN);
+			Integer energyCount = reconEnergyBins.get(energyBin);
+			if(energyCount == null) { reconEnergyBins.put(energyBin, 1); }
+			else { reconEnergyBins.put(energyBin, energyCount + 1); }
+		}
+		
+    	// Update the plotting information for SSP clusters.
+		for(SSPCluster cluster : sspClusters) {
+			// Update the hit count bin data.
+			Integer hitCountCount = sspHitCountBins.get(cluster.getHitCount());
+			if(hitCountCount == null) { sspHitCountBins.put(cluster.getHitCount(), 1); }
+			else { sspHitCountBins.put(cluster.getHitCount(), hitCountCount + 1); }
+			
+			// Update the position bin data.
+			Point clusterPosition = TriggerDiagnosticUtil.getClusterPosition(cluster);
+			Integer positionCount = sspPositionBins.get(clusterPosition);
+			if(positionCount == null) { sspPositionBins.put(clusterPosition, 1); }
+			else { sspPositionBins.put(clusterPosition, positionCount + 1); }
+			
+			// Update the energy bin data.
+			int energyBin = (int) Math.floor(cluster.getEnergy() / ENERGY_BIN);
+			Integer energyCount = sspEnergyBins.get(energyBin);
+			if(energyCount == null) { sspEnergyBins.put(energyBin, 1); }
+			else { sspEnergyBins.put(energyBin, energyCount + 1); }
+		}
+		
+		// Update the plotting information for SSP/reconstructed cluster
+		// pairs.
+		for(ClusterMatchedPair pair : event.getMatchedPairs()) {
+			// If one of the pairs is null, then it is unmatched cluster
+			// and may be skipped.
+			if(pair.getReconstructedCluster() == null || pair.getSSPCluster() == null) {
+				continue;
+			}
+			
+			// Populate the bins for the "all" plots.
+			// Update the match time plots.
+			int timeBin = (int) Math.floor(TriggerDiagnosticUtil.getClusterTime(pair.getReconstructedCluster()) / TIME_BIN);
+			Integer timeCount = allTimeBins.get(timeBin);
+			if(timeCount == null) { allTimeBins.put(timeBin, 1); }
+			else { allTimeBins.put(timeBin, timeCount + 1); }
+			
+			// Update the energy/hit difference plots.
+			int hitBin = getHitCountDifference(pair.getSSPCluster(), pair.getReconstructedCluster());
+			int energyBin = (int) Math.floor(getEnergyPercentDifference(pair.getSSPCluster(), pair.getReconstructedCluster()) / ENERGY_BIN);
+			Point henergyBin = new Point(hitBin, energyBin);
+			Integer henergyCount = allHenergyBins.get(henergyBin);
+			if(henergyCount == null) { allHenergyBins.put(henergyBin, 1); }
+			else { allHenergyBins.put(henergyBin, henergyCount + 1); }
+			
+			// Populate the bins for the "fail" plots.
+			if(!pair.isMatch()) {
+				// Update the failed cluster position bins.
+				Point clusterPosition = TriggerDiagnosticUtil.getClusterPosition(pair.getReconstructedCluster());
+				Integer positionCount = failPositionBins.get(clusterPosition);
+				if(positionCount == null) { failPositionBins.put(clusterPosition, 1); }
+				else { failPositionBins.put(clusterPosition, positionCount + 1); }
+				
+				// Update the failed match time plots.
+				timeCount = failTimeBins.get(timeBin);
+				if(timeCount == null) { failTimeBins.put(timeBin, 1); }
+				else { failTimeBins.put(timeBin, timeCount + 1); }
+				
+				// Update the failed energy/hit difference plots.
+				henergyCount = failHenergyBins.get(henergyBin);
+				if(henergyCount == null) { failHenergyBins.put(henergyBin, 1); }
+				else { failHenergyBins.put(henergyBin, henergyCount + 1); }
+			}
+		}
+    }
+    
+	/**
+	 * Clears all statistical information and resets the object ot its
+	 * default, empty state.
+	 */
+	public void clear() {
+		// Clear statistical data.
+		sspClusters   = 0;
+		reconClusters = 0;
+		matches       = 0;
+		failEnergy    = 0;
+		failPosition  = 0;
+		failHitCount  = 0;
+		
+		// Clear plot collections.
+		sspHitCountBins.clear();
+		reconHitCountBins.clear();
+		sspPositionBins.clear();
+		reconPositionBins.clear();
+		sspEnergyBins.clear();
+		reconEnergyBins.clear();
+		failPositionBins.clear();
+		allHenergyBins.clear();
+		failHenergyBins.clear();
+		allTimeBins.clear();
+		failTimeBins.clear();
+	}
+	
+	/**
+	 * Gets the number of cluster pairs stored in this event that are
+	 * marked with energy fail states.
+	 * @return Returns the number of instances of this state as an
+	 * <code>int</code> primitive.
+	 */
+	public int getEnergyFailures() {
+		return failEnergy;
+	}
+	
+	/**
+	 * Gets the number of cluster pairs stored in this event that are
+	 * marked with hit count fail states.
+	 * @return Returns the number of instances of this state as an
+	 * <code>int</code> primitive.
+	 */
+	public int getHitCountFailures() {
+		return failHitCount;
+	}
+	
+	/**
+	 * Gets the number of cluster pairs stored in this event that are
+	 * marked with position fail states.
+	 * @return Returns the number of instances of this state as an
+	 * <code>int</code> primitive.
+	 */
+	public int getMatches() {
+		return matches;
+	}
+	
+	/**
+	 * Gets the number of cluster pairs stored in this event that are
+	 * marked with position fail states.
+	 * @return Returns the number of instances of this state as an
+	 * <code>int</code> primitive.
+	 */
+	public int getPositionFailures() {
+		return failPosition;
+	}
+	
+	/**
+	 * Gets the total number of verifiable reconstructed clusters seen.
+     * @return Returns the cluster count as an <code>int</code>
+     * primitive.
+	 */
+    public int getReconClusterCount() {
+    	return reconClusters;
+    }
+    
+    /**
+     * Gets the total number of SSP bank clusters seen.
+     * @return Returns the cluster count as an <code>int</code>
+     * primitive.
+     */
+    public int getSSPClusterCount() {
+    	return sspClusters;
+    }
+    
+	/**
+	 * Gets the difference in hit count between an SSP cluster and a
+	 * reconstructed cluster.
+	 * @param sspCluster - The SSP cluster.
+	 * @param reconCluster - The reconstructed cluster.
+	 * @return Returns the difference as an <code>int</code> primitive.
+	 */
+	private static final int getHitCountDifference(SSPCluster sspCluster, Cluster reconCluster) {
+		return sspCluster.getHitCount() - TriggerDiagnosticUtil.getHitCount(reconCluster);
+	}
+	
+	/**
+	 * Solves the equation <code>|E_ssp / E_recon|</code>.
+	 * @param sspCluster - The SSP cluster.
+	 * @param reconCluster - The reconstructed cluster.
+	 * @return Returns the solution to the equation as a <code>double
+	 * </code> primitive.
+	 */
+	private static final double getEnergyPercentDifference(SSPCluster sspCluster, Cluster reconCluster) {
+		return Math.abs((sspCluster.getEnergy() / reconCluster.getEnergy()));
+	}
+}

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/DiagSnapshot.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/DiagSnapshot.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/DiagSnapshot.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,8 @@
+package org.hps.users.kmccarty;
+
+public class DiagSnapshot {
+	// UNDER CONSTRUCTION
+	// This is an empty file for now; it is still being built, but the
+	// table and driver won't work unless this object exists, so the
+	// empty class is included for the time being.
+}

Modified: java/trunk/users/src/main/java/org/hps/users/kmccarty/PairTrigger.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/PairTrigger.java	(original)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/PairTrigger.java	Tue Mar  3 16:01:55 2015
@@ -154,4 +154,13 @@
 	public void setStateTimeCoincidence(boolean state) {
 		setCutState(PAIR_TIME_COINCIDENCE, state);
 	}
+	
+	@Override
+	public String toString() {
+		return String.format("EClusterLow: %d; EClusterHigh %d; HitCount: %d; ESumLow: %d, ESumHigh: %d, EDiff: %d, ESlope: %d, Coplanarity: %d",
+				getStateClusterEnergyLow() ? 1 : 0, getStateClusterEnergyHigh() ? 1 : 0,
+				getStateHitCount() ? 1 : 0, getStateEnergySumLow() ? 1 : 0,
+				getStateEnergySumHigh() ? 1 : 0, getStateEnergyDifference() ? 1 : 0,
+				getStateEnergySlope() ? 1 : 0, getStateCoplanarity() ? 1 : 0);
+	}
 }

Modified: java/trunk/users/src/main/java/org/hps/users/kmccarty/SinglesTrigger.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/SinglesTrigger.java	(original)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/SinglesTrigger.java	Tue Mar  3 16:01:55 2015
@@ -143,4 +143,11 @@
 	public void setStateClusterEnergyHigh(boolean state) {
 		setCutState(CLUSTER_TOTAL_ENERGY_HIGH, state);
 	}
+	
+	@Override
+	public String toString() {
+		return String.format("EClusterLow: %d; EClusterHigh %d; HitCount: %d",
+				getStateClusterEnergyLow() ? 1 : 0, getStateClusterEnergyHigh() ? 1 : 0,
+				getStateHitCount() ? 1 : 0);
+	}
 }

Modified: java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticDriver.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticDriver.java	(original)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticDriver.java	Tue Mar  3 16:01:55 2015
@@ -17,6 +17,7 @@
 import org.hps.readout.ecal.triggerbank.AbstractIntData;
 import org.hps.readout.ecal.triggerbank.SSPCluster;
 import org.hps.readout.ecal.triggerbank.SSPData;
+import org.hps.readout.ecal.triggerbank.SSPNumberedTrigger;
 import org.hps.readout.ecal.triggerbank.SSPPairTrigger;
 import org.hps.readout.ecal.triggerbank.SSPSinglesTrigger;
 import org.hps.readout.ecal.triggerbank.SSPTrigger;
@@ -62,16 +63,8 @@
 	private boolean performPairTriggerVerification = true;
 	
 	// Efficiency tracking variables.
-	private static final int GLOBAL      = 0;
-	private static final int LOCAL       = 1;
-	private static final int EVENT       = 2;
-	
-	private int[] clusterMatched      = new int[3];
-	private int[] clusterSSPCount     = new int[3];
-	private int[] clusterFailEnergy   = new int[3];
-	private int[] clusterReconCount   = new int[3];
-	private int[] clusterFailPosition = new int[3];
-	private int[] clusterFailHitCount = new int[3];
+	private ClusterMatchStatus clusterRunStats = new ClusterMatchStatus();
+	private ClusterMatchStatus clusterLocalStats = new ClusterMatchStatus();
 	
 	private int singlesSSPTriggers = 0;
 	private int singlesReconMatched = 0;
@@ -127,6 +120,16 @@
     private boolean printPairTriggerInternalFail = true;
     private StringBuffer outputBuffer = new StringBuffer();
     
+    // Cut index arrays for trigger verification.
+	private static final int ENERGY_MIN = 0;
+	private static final int ENERGY_MAX = 1;
+	private static final int HIT_COUNT = 2;
+	private static final int ENERGY_SUM = 0;
+	private static final int ENERGY_DIFF = 1;
+	private static final int ENERGY_SLOPE = 2;
+	private static final int COPLANARITY = 3;
+	private static final int TIME = 4;
+    
 	/**
 	 * Define the trigger modules. This should be replaced by parsing
 	 * the DAQ configuration at some point.
@@ -151,12 +154,12 @@
 		System.out.println("=== Cluster/Trigger Verification Settings ============================");
 		System.out.println("======================================================================");
 		
-		/* Runs 2040+
 		// Set the FADC settings.
-		nsa = 240;
-		nsb = 12;
+		nsa = 100;
+		nsb = 20;
 		windowWidth = 400;
 		
+		/*
 		// Define the first singles trigger.
 		singlesTrigger[0] = new TriggerModule();
 		singlesTrigger[0].setCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_LOW, 0.010);
@@ -271,14 +274,15 @@
 		
 		// Print the cluster verification data.
 		System.out.println("Cluster Verification:");
-		System.out.printf("\tRecon Clusters        :: %d%n", clusterReconCount[GLOBAL]);
-		System.out.printf("\tSSP Clusters          :: %d%n", clusterSSPCount[GLOBAL]);
-		System.out.printf("\tClusters Matched      :: %d%n", clusterMatched[GLOBAL]);
-		System.out.printf("\tFailed (Position)     :: %d%n", clusterFailPosition[GLOBAL]);
-		System.out.printf("\tFailed (Energy)       :: %d%n", clusterFailEnergy[GLOBAL]);
-		System.out.printf("\tFailed (Hit Count)    :: %d%n", clusterFailHitCount[GLOBAL]);
-		if(clusterReconCount[GLOBAL] == 0) { System.out.printf("\tCluster Efficiency    :: N/A%n"); }
-		else { System.out.printf("\tCluster Efficiency :: %7.3f%%%n", 100.0 * clusterMatched[GLOBAL] / clusterReconCount[GLOBAL]); }
+	
+		System.out.printf("\tRecon Clusters        :: %d%n", clusterRunStats.getReconClusterCount());
+		System.out.printf("\tSSP Clusters          :: %d%n", clusterRunStats.getSSPClusterCount());
+		System.out.printf("\tClusters Matched      :: %d%n", clusterRunStats.getMatches());
+		System.out.printf("\tFailed (Position)     :: %d%n", clusterRunStats.getPositionFailures());
+		System.out.printf("\tFailed (Energy)       :: %d%n", clusterRunStats.getEnergyFailures());
+		System.out.printf("\tFailed (Hit Count)    :: %d%n", clusterRunStats.getHitCountFailures());
+		if(clusterRunStats.getReconClusterCount() == 0) { System.out.printf("\tCluster Efficiency    :: N/A%n"); }
+		else { System.out.printf("\tCluster Efficiency :: %7.3f%%%n", 100.0 * clusterRunStats.getMatches() / clusterRunStats.getReconClusterCount()); }
 		
 		// Print the singles trigger verification data.
 		int spaces = getPrintSpaces(singlesSSPTriggers, singlesReconTriggers,
@@ -382,6 +386,16 @@
 		// ==========================================================
 		// ==== Initialize the Event ================================
 		// ==========================================================
+        
+        // Print the verification header.
+		println();
+		println();
+		println("======================================================================");
+		println("==== Cluster/Trigger Verification ====================================");
+		println("======================================================================");
+		
+		// Increment the total event count.
+		totalEvents++;
 		
 		// Reset the output buffer and print flags.
 		outputBuffer = new StringBuffer();
@@ -426,19 +440,86 @@
             }
         }
         */
+		
+		
+		
+		// ==========================================================
+		// ==== Obtain SSP and TI Banks =============================
+		// ==========================================================
+		
+		// Get the SSP clusters.
+		if(event.hasCollection(GenericObject.class, bankCollectionName)) {
+			// Get the bank list.
+			List<GenericObject> bankList = event.get(GenericObject.class, bankCollectionName);
+			
+			// Search through the banks and get the SSP and TI banks.
+			for(GenericObject obj : bankList) {
+				// If this is an SSP bank, parse it.
+				if(AbstractIntData.getTag(obj) == SSPData.BANK_TAG) {
+					sspBank = new SSPData(obj);
+				}
+				
+				// Otherwise, if this is a TI bank, parse it.
+				else if(AbstractIntData.getTag(obj) == TIData.BANK_TAG) {
+					tiBank = new TIData(obj);
+					
+					if(tiBank.isPulserTrigger()) { println("Trigger type :: Pulser"); }
+					else if(tiBank.isSingle0Trigger() || tiBank.isSingle1Trigger()) { println("Trigger type :: Singles"); }
+					else if(tiBank.isPair0Trigger() || tiBank.isPair1Trigger()) { println("Trigger type :: Pair"); }
+					else if(tiBank.isCalibTrigger()) { println("Trigger type :: Cosmic"); }
+				}
+			}
+			
+			// If there is an SSP bank, get the list of SSP clusters.
+			if(sspBank != null) {
+				sspClusters = sspBank.getClusters();
+				if(sspClusters.size() == 1) {
+					println("1 SSP cluster found.");
+				} else {
+					printf("%d SSP clusters found.%n", sspClusters.size());
+				}
+			}
+		}
+		
+		
+		
+		// ==========================================================
+		// ==== Establish Event Integrity ===========================
+		// ==========================================================
+		
+		// Check that all of the required objects are present.
+		if(sspBank == null) {
+			println("No SSP bank found for this event. No verification will be performed.");
+			if(verbose) { System.out.println(outputBuffer.toString()); }
+			return;
+		} if(tiBank == null) {
+			println("No TI bank found for this event. No verification will be performed.");
+			if(verbose) { System.out.println(outputBuffer.toString()); }
+			return;
+		}
+		
+		
+		
+		// ==========================================================
+		// ==== Check the Noise Level ===============================
+		// ==========================================================
+		
+		// Check if there are hits.
+		if(event.hasCollection(CalorimeterHit.class, hitCollectionName)) {
+			// Check if there are more hits than the noise threshold.
+			if(event.get(CalorimeterHit.class, hitCollectionName).size() >= noiseThreshold) {
+				noiseEvents++;
+				println("Noise event detected. Skipping event...");
+				if(verbose) { System.out.println(outputBuffer.toString()); }
+				return;
+			}
+		}
         
         
         
 		// ==========================================================
 		// ==== Obtain Reconstructed Clusters =======================
 		// ==========================================================
-        
-        // Print the verification header.
-		println();
-		println();
-		println("======================================================================");
-		println("==== Cluster/Trigger Verification ====================================");
-		println("======================================================================");
 		
 		// Clear the list of triggers from previous events.
 		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
@@ -519,80 +600,6 @@
 		
 		
 		// ==========================================================
-		// ==== Obtain SSP and TI Banks =============================
-		// ==========================================================
-		
-		// Get the SSP clusters.
-		if(event.hasCollection(GenericObject.class, bankCollectionName)) {
-			// Get the bank list.
-			List<GenericObject> bankList = event.get(GenericObject.class, bankCollectionName);
-			
-			// Search through the banks and get the SSP and TI banks.
-			for(GenericObject obj : bankList) {
-				// If this is an SSP bank, parse it.
-				if(AbstractIntData.getTag(obj) == SSPData.BANK_TAG) {
-					sspBank = new SSPData(obj);
-				}
-				
-				// Otherwise, if this is a TI bank, parse it.
-				else if(AbstractIntData.getTag(obj) == TIData.BANK_TAG) {
-					tiBank = new TIData(obj);
-					
-					if(tiBank.isPulserTrigger()) { println("Trigger type :: Pulser"); }
-					else if(tiBank.isSingle0Trigger() || tiBank.isSingle1Trigger()) { println("Trigger type :: Singles"); }
-					else if(tiBank.isPair0Trigger() || tiBank.isPair1Trigger()) { println("Trigger type :: Pair"); }
-					else if(tiBank.isCalibTrigger()) { println("Trigger type :: Cosmic"); }
-				}
-			}
-			
-			// If there is an SSP bank, get the list of SSP clusters.
-			if(sspBank != null) {
-				sspClusters = sspBank.getClusters();
-				if(sspClusters.size() == 1) {
-					println("1 SSP cluster found.");
-				} else {
-					printf("%d SSP clusters found.%n", sspClusters.size());
-				}
-			}
-		}
-		
-		
-		
-		// ==========================================================
-		// ==== Establish Event Integrity ===========================
-		// ==========================================================
-		
-		// Check that all of the required objects are present.
-		if(sspBank == null) {
-			println("No SSP bank found for this event. No verification will be performed.");
-			return;
-		} if(tiBank == null) {
-			println("No TI bank found for this event. No verification will be performed.");
-			return;
-		}
-		
-		
-		
-		// ==========================================================
-		// ==== Check the Noise Level ===============================
-		// ==========================================================
-		
-		// Increment the total event count.
-		totalEvents++;
-		
-		// Check if there are hits.
-		if(event.hasCollection(CalorimeterHit.class, hitCollectionName)) {
-			// Check if there are more hits than the noise threshold.
-			if(event.get(CalorimeterHit.class, hitCollectionName).size() >= noiseThreshold) {
-				noiseEvents++;
-				println("Noise event detected. Skipping event...");
-				return;
-			}
-		}
-		
-		
-		
-		// ==========================================================
 		// ==== Perform Event Verification ==========================
 		// ==========================================================
 		
@@ -638,13 +645,8 @@
 			// Write a snapshot of the driver to the event stream.
 			// TODO: Readout the snapshot!!
 			
-			// Reset the local variables to zero.
-			clusterMatched[LOCAL]      = 0;
-			clusterSSPCount[LOCAL]     = 0;
-			clusterFailEnergy[LOCAL]   = 0;
-			clusterReconCount[LOCAL]   = 0;
-			clusterFailPosition[LOCAL] = 0;
-			clusterFailHitCount[LOCAL] = 0;
+			// Clear the local statistical data.
+			clusterLocalStats.clear();
 		}
 	}
 	
@@ -721,8 +723,6 @@
 		
 		// Track the number of cluster pairs that were matched and that
 		// failed by failure type.
-		clusterSSPCount[EVENT]     = 0;
-		clusterReconCount[EVENT]   = 0;
 		ClusterMatchEvent event = new ClusterMatchEvent();
 		
 		
@@ -751,9 +751,6 @@
 			
 			// Add the cluster to the list.
 			reconList.add(reconCluster);
-			
-			// Count the reconstructed clusters.
-			clusterReconCount[EVENT]++;
 		}
 		
 		// Populate the SSP cluster map.
@@ -770,9 +767,6 @@
 			
 			// Add the cluster to the list.
 			sspList.add(sspCluster);
-			
-			// Count the SSP clusters.
-			clusterSSPCount[EVENT]++;
 		}
 		
 		
@@ -797,18 +791,17 @@
 			// reason of position. The remainder of the loop may be
 			// skipped, since there is nothing to check.
 			if(sspList == null || sspList.isEmpty()) {
-				clusterFailPosition[EVENT] += reconList.size();
 				clusterFail = true;
+				for(Cluster cluster : reconList) {
+					event.pairFailPosition(cluster, null);
+				}
 				continue positionLoop;
 			}
 			
 			// If there are more reconstructed clusters than there are
 			// SSP clusters, than a number equal to the difference must
 			// fail by means of positions.
-			if(sspList.size() < reconList.size()) {
-				clusterFail = true;
-				clusterFailPosition[EVENT] += (sspList.size() - reconList.size());
-			}
+			if(sspList.size() < reconList.size()) { clusterFail = true; }
 			
 			// Get all possible permutations of SSP clusters.
 			List<List<Pair>> permutations = getPermutations(reconList, sspList);
@@ -835,9 +828,6 @@
 				// Try to match each pair.
 				pairLoop:
 				for(Pair pair : pairs) {
-					// Track the state of the current pair.
-					//int pairState = STATE_CLUSTER_UNDEFINED;
-					
 					// Print the current reconstructed/SSP cluster pair.
 					printf("\tP%d :: %s --> %s", permIndex,
 							pair.reconCluster == null ? "None" : clusterToString(pair.reconCluster),
@@ -861,16 +851,13 @@
 						if(pair.sspCluster.getHitCount() >= pair.reconCluster.getCalorimeterHits().size() - hitAcceptance &&
 								pair.sspCluster.getHitCount() <= pair.reconCluster.getCalorimeterHits().size() + hitAcceptance) {
 							// Designate the pair as a match.
-							//pairState = STATE_CLUSTER_SUCCESS_MATCH;
 							perm.pairMatch(pair.reconCluster, pair.sspCluster);
 							printf(" [ %18s ]%n", "success: matched");
 						} else {
-							//pairState = STATE_CLUSTER_FAIL_HIT_COUNT;
 							perm.pairFailHitCount(pair.reconCluster, pair.sspCluster);
 							printf(" [ %18s ]%n", "failure: hit count");
 						} // End hit count check.
 					} else {
-						//pairState = STATE_CLUSTER_FAIL_ENERGY;
 						perm.pairFailEnergy(pair.reconCluster, pair.sspCluster);
 						printf(" [ %18s ]%n", "failure: energy");
 					} // End energy check.
@@ -900,20 +887,8 @@
 		} // End Crystal Position Loop
 		
 		// Add the event results to the global results.
-		clusterMatched[GLOBAL]      += event.getMatches();
-		clusterFailPosition[GLOBAL] += clusterFailPosition[EVENT];
-		clusterFailEnergy[GLOBAL]   += event.getEnergyFailures();
-		clusterFailHitCount[GLOBAL] += event.getHitCountFailures();
-		clusterReconCount[GLOBAL]   += clusterReconCount[EVENT];
-		clusterSSPCount[GLOBAL]     += clusterSSPCount[EVENT];
-		
-		// Add the event results to the local results.
-		clusterMatched[LOCAL]      += event.getMatches();
-		clusterFailPosition[LOCAL] += clusterFailPosition[EVENT];
-		clusterFailEnergy[LOCAL]   += event.getEnergyFailures();
-		clusterFailHitCount[LOCAL] += event.getHitCountFailures();
-		clusterReconCount[LOCAL]   += clusterReconCount[EVENT];
-		clusterSSPCount[LOCAL]     += clusterSSPCount[EVENT];
+		clusterRunStats.addEvent(event, reconClusters, sspClusters);
+		clusterLocalStats.addEvent(event, reconClusters, sspClusters);
 		
 		
 		
@@ -954,18 +929,24 @@
 		}
 		 else { println("\tNone"); }
 		
+		// Get the number of position failures.
+		int failPosition = event.getPositionFailures();
+		if(sspClusters == null || sspClusters.isEmpty()) {
+			failPosition = (reconClusters == null ? 0 : reconClusters.size());
+		}
+		
 		// Print event statistics.
 		println();
 		println("Event Statistics:");
 		printf("\tRecon Clusters     :: %d%n", reconClusters.size());
 		printf("\tClusters Matched   :: %d%n", event.getMatches());
-		printf("\tFailed (Position)  :: %d%n", clusterFailPosition[EVENT]);
+		printf("\tFailed (Position)  :: %d%n", failPosition);
 		printf("\tFailed (Energy)    :: %d%n", event.getEnergyFailures());
 		printf("\tFailed (Hit Count) :: %d%n", event.getHitCountFailures());
 		printf("\tCluster Efficiency :: %3.0f%%%n", 100.0 * event.getMatches() / reconClusters.size());
 		
 		// Note whether there was a cluster match failure.
-		if(clusterMatched[EVENT] - reconClusters.size() != 0) {
+		if(event.getMatches() - reconClusters.size() != 0) {
 			clusterFail = true;
 		}
 	}
@@ -977,15 +958,66 @@
 	 * simulated on reconstructed clusters to measure trigger efficiency.
 	 */
 	private void singlesTriggerVerification() {
-		// ==========================================================
-		// ==== Initialize Singles Trigger Verification =============
+		// Create lists of generic triggers.
+		List<List<? extends Trigger<?>>> sspTriggerList = new ArrayList<List<? extends Trigger<?>>>(2);
+		List<List<? extends Trigger<?>>> reconTriggerList = new ArrayList<List<? extends Trigger<?>>>(2);
+		
+		// Convert the simulated triggers to generic versions and add
+		// them to the generic list.
+		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
+			// Get the generic trigger list.
+			List<? extends Trigger<?>> sspTriggers = sspSinglesTriggers.get(triggerNum);
+			List<? extends Trigger<?>> reconTriggers = reconSinglesTriggers.get(triggerNum);
+			
+			// Add it to the generic list.
+			sspTriggerList.add(sspTriggers);
+			reconTriggerList.add(reconTriggers);
+		}
+		
+		// Run generic trigger verification.
+		triggerVerification(sspTriggerList, reconTriggerList, true);
+	}
+	
+	/**
+	 * Checks triggers simulated on SSP clusters against the SSP bank's
+	 * reported triggers to verify that the trigger is correctly applying
+	 * cuts to the clusters it sees. Additionally compares triggers
+	 * simulated on reconstructed clusters to measure trigger efficiency.
+	 */
+	private void pairTriggerVerification() {
+		// Create lists of generic triggers.
+		List<List<? extends Trigger<?>>> sspTriggerList = new ArrayList<List<? extends Trigger<?>>>(2);
+		List<List<? extends Trigger<?>>> reconTriggerList = new ArrayList<List<? extends Trigger<?>>>(2);
+		
+		// Convert the simulated triggers to generic versions and add
+		// them to the generic list.
+		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
+			// Get the generic trigger list.
+			List<? extends Trigger<?>> sspTriggers = sspPairsTriggers.get(triggerNum);
+			List<? extends Trigger<?>> reconTriggers = reconPairsTriggers.get(triggerNum);
+			
+			// Add it to the generic list.
+			sspTriggerList.add(sspTriggers);
+			reconTriggerList.add(reconTriggers);
+		}
+		
+		// Run generic trigger verification.
+		triggerVerification(sspTriggerList, reconTriggerList, false);
+	}
+	
+	private void triggerVerification(List<List<? extends Trigger<?>>> sspTriggerList, 
+			List<List<? extends Trigger<?>>> reconTriggerList, boolean isSingles) {
+		
+		// ==========================================================
+		// ==== Initialize Trigger Verification =====================
 		// ==========================================================
 		
 		// Print the cluster verification header.
 		println();
 		println();
 		println("======================================================================");
-		println("=== Singles Trigger Verification =====================================");
+		if(isSingles) { println("=== Singles Trigger Verification ====================================="); }
+		else { println("=== Pair Trigger Verification ========================================"); }
 		println("======================================================================");
 		
 		// Track the number of triggers seen and the number found.
@@ -996,10 +1028,7 @@
 		
 		// Track the number of times a given cut caused a trigger to
 		// fail to match.
-		int[] eventEnergyMin = new int[2];
-		int[] eventEnergyMax = new int[2];
-		int[] eventHitCount = new int[2];
-		int[] eventTime = new int[2];
+		int[][] triggerComp = new int[5][2];
 		
 		
 		
@@ -1008,52 +1037,45 @@
 		// ==========================================================
 		
 		// Get the list of triggers reported by the SSP.
-		List<SSPSinglesTrigger> sspTriggers = sspBank.getSinglesTriggers();
+		List<? extends SSPNumberedTrigger> sspTriggers;
+		if(isSingles) { sspTriggers = sspBank.getSinglesTriggers(); }
+		else { sspTriggers = sspBank.getPairTriggers(); }
 		
 		// Output the SSP cluster singles triggers.
 		println();
-		println("SSP Cluster Singles Triggers");
+		println("SSP Cluster " + (isSingles ? "Singles" : "Pair") + " Triggers");
 		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(SinglesTrigger<SSPCluster> simTrigger : sspSinglesTriggers.get(triggerNum)) {
-				printf("\tTrigger %d :: %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d%n",
-						(triggerNum + 1), clusterPositionString(simTrigger.getTriggerSource()),
-						simTrigger.getStateClusterEnergyLow() ? 1 : 0,
-						simTrigger.getStateClusterEnergyHigh() ? 1 : 0,
-						simTrigger.getStateHitCount() ? 1 : 0);
-			}
-		}
-		if(sspSinglesTriggers.get(0).size() + sspSinglesTriggers.get(1).size() == 0) {
+			for(Trigger<?> simTrigger : sspTriggerList.get(triggerNum)) {
+				printf("\tTrigger %d :: %s :: %s%n",
+						(triggerNum + 1), triggerPositionString(simTrigger),
+						simTrigger.toString());
+			}
+		}
+		if(sspTriggerList.get(0).size() + sspTriggerList.get(1).size() == 0) {
 			println("\tNone");
 		}
 		
 		// Output the reconstructed cluster singles triggers.
-		println("Reconstructed Cluster Singles Triggers");
+		println("Reconstructed Cluster " + (isSingles ? "Singles" : "Pair") + " Triggers");
 		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(SinglesTrigger<Cluster> simTrigger : reconSinglesTriggers.get(triggerNum)) {
-				printf("\tTrigger %d :: %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d%n",
-						(triggerNum + 1), clusterPositionString(simTrigger.getTriggerSource()),
-						simTrigger.getStateClusterEnergyLow() ? 1 : 0,
-						simTrigger.getStateClusterEnergyHigh() ? 1 : 0,
-						simTrigger.getStateHitCount() ? 1 : 0);
-			}
-		}
-		if(reconSinglesTriggers.get(0).size() + reconSinglesTriggers.get(1).size() == 0) {
+			for(Trigger<?> simTrigger : reconTriggerList.get(triggerNum)) {
+				printf("\tTrigger %d :: %s :: %s%n",
+						(triggerNum + 1), triggerPositionString(simTrigger),
+						simTrigger.toString());
+			}
+		}
+		if(reconTriggerList.get(0).size() + reconTriggerList.get(1).size() == 0) {
 			println("\tNone");
 		}
 		
 		// Output the SSP reported triggers.
-		println("SSP Reported Singles Triggers");
-		for(SSPSinglesTrigger sspTrigger : sspTriggers) {
+		println("SSP Reported " + (isSingles ? "Singles" : "Pair") + " Triggers");
+		for(SSPTrigger sspTrigger : sspTriggers) {
 			// Increment the number of SSP cluster singles triggers.
 			sspReportedTriggers++;
 			
-			// Get the trigger properties.
-			int triggerNum = sspTrigger.isFirstTrigger() ? 1 : 2;
-			
 			// Print the trigger.
-			printf("\tTrigger %d :: %3d ns :: EClusterLow: %d; EClusterHigh %d; HitCount: %d%n",
-					triggerNum, sspTrigger.getTime(), sspTrigger.passCutEnergyMin() ? 1 : 0,
-					sspTrigger.passCutEnergyMax() ? 1 : 0, sspTrigger.passCutHitCount() ? 1 : 0);
+			printf("\t%s%n", sspTrigger.toString());
 		}
 		if(sspReportedTriggers == 0) { println("\tNone"); }
 		
@@ -1066,21 +1088,22 @@
 		// Track which SSP triggers have been matched to avoid matching
 		// multiple reconstructed SSP cluster triggers to the same SSP
 		// trigger.
-		Set<SSPSinglesTrigger> sspTriggerSet = new HashSet<SSPSinglesTrigger>();
-		Set<SinglesTrigger<SSPCluster>> simTriggerSet = new HashSet<SinglesTrigger<SSPCluster>>();
+		Set<SSPNumberedTrigger> sspTriggerSet = new HashSet<SSPNumberedTrigger>();
+		Set<Trigger<?>> simTriggerSet = new HashSet<Trigger<?>>();
 		
 		// Track the number of SSP reported triggers that are found in
 		// excess of the SSP simulated triggers.
-		int extraTriggers = sspTriggers.size() - (sspSinglesTriggers.get(0).size() + sspSinglesTriggers.get(1).size());
+		int extraTriggers = sspTriggers.size() - (sspTriggerList.get(0).size() + sspTriggerList.get(1).size());
 		if(extraTriggers > 0) {
 			sspReportedExtras += extraTriggers;
-			singlesInternalFail = true;
+			if(isSingles) { singlesInternalFail = true; }
+			else { pairInternalFail = true; }
 		}
 		
 		// Iterate over the triggers.
 		println();
 		println("SSP Reported Trigger --> SSP Cluster Trigger Match Status");
-		for(SSPSinglesTrigger sspTrigger : sspTriggers) {
+		for(SSPNumberedTrigger sspTrigger : sspTriggers) {
 			// Get the trigger information.
 			int triggerNum = sspTrigger.isFirstTrigger() ? 0 : 1;
 			boolean matchedTrigger = false;
@@ -1088,14 +1111,14 @@
 			// Iterate over the SSP cluster simulated triggers and
 			// look for a trigger that matches.
 			matchLoop:
-			for(SinglesTrigger<SSPCluster> simTrigger : sspSinglesTriggers.get(triggerNum)) {
+			for(Trigger<?> simTrigger : sspTriggerList.get(triggerNum)) {
 				// If the current SSP trigger has already been
 				// matched, skip it.
 				if(sspTriggerSet.contains(sspTrigger)) { continue matchLoop; }
 				
 				// Otherwise, check whether the reconstructed SSP
 				// cluster trigger matches the SSP trigger.
-				if(compareSSPSinglesTriggers(sspTrigger, simTrigger)) {
+				if(compareTriggers(sspTrigger, simTrigger)) {
 					matchedTrigger = true;
 					sspTriggerSet.add(sspTrigger);
 					simTriggerSet.add(simTrigger);
@@ -1103,10 +1126,7 @@
 					break matchLoop;
 				}
 				
-				printf("\tTrigger %d :: %3d :: EClusterLow: %d; EClusterHigh %d; HitCount: %d :: Matched: %5b%n",
-						(triggerNum + 1), sspTrigger.getTime(), sspTrigger.passCutEnergyMin() ? 1 : 0,
-						sspTrigger.passCutEnergyMax() ? 1 : 0, sspTrigger.passCutHitCount() ? 1 : 0,
-						matchedTrigger);
+				printf("\t%s :: Matched: %5b%n", sspTrigger.toString(), matchedTrigger);
 			}
 		}
 		
@@ -1114,23 +1134,25 @@
 		// unmatched SSP reported trigger that most closely matches it.
 		simLoop:
 		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(SinglesTrigger<SSPCluster> simTrigger : sspSinglesTriggers.get(triggerNum)) {
+			for(Trigger<?> simTrigger : sspTriggerList.get(triggerNum)) {
 				// Check whether this trigger has already been matched
 				// or not. If it has been matched, skip it.
 				if(simTriggerSet.contains(simTrigger)) { continue simLoop; }
 				
+				// Get the trigger time for the simulated trigger.
+				double simTime = getTriggerTime(simTrigger);
+				
 				// Track the match statistics for each reported trigger
 				// so that the closest match may be found.
 				int numMatched = -1;
-				boolean foundBest = false;
-				boolean[] matchedCut = new boolean[3];
+				boolean[] matchedCut = null;
 				
 				// Iterate over the reported triggers to find a match.
 				reportedLoop:
-				for(SSPSinglesTrigger sspTrigger : sspTriggers) {
+				for(SSPNumberedTrigger sspTrigger : sspTriggers) {
 					// If the two triggers have different times, this
 					// trigger should be skipped.
-					if(sspTrigger.getTime() != simTrigger.getTriggerSource().getTime()) {
+					if(sspTrigger.getTime() != simTime) {
 						continue reportedLoop;
 					}
 					
@@ -1139,10 +1161,7 @@
 					if(sspTriggerSet.contains(sspTrigger)) { continue reportedLoop; }
 					
 					// Check each of the cuts.
-					boolean[] tempMatchedCut = new boolean[3];
-					tempMatchedCut[0] = (simTrigger.getStateClusterEnergyLow()  == sspTrigger.passCutEnergyMin());
-					tempMatchedCut[1] = (simTrigger.getStateClusterEnergyHigh() == sspTrigger.passCutEnergyMax());
-					tempMatchedCut[2] = (simTrigger.getStateHitCount()          == sspTrigger.passCutHitCount());
+					boolean[] tempMatchedCut = triggerCutMatch(simTrigger, sspTrigger);
 					
 					// Check each cut and see if this is a closer match
 					// than the previous best match.
@@ -1152,7 +1171,6 @@
 					// If the number of matched cuts exceeds the old
 					// best result, this becomes the new best result.
 					if(tempNumMatched > numMatched) {
-						foundBest = true;
 						numMatched = tempNumMatched;
 						matchedCut = tempMatchedCut;
 					}
@@ -1160,18 +1178,19 @@
 				
 				// If some match was found, note what caused it to not
 				// qualify as a complete match.
-				if(foundBest) {
-					if(!matchedCut[0]) { eventEnergyMin[triggerNum]++; }
-					if(!matchedCut[1]) { eventEnergyMax[triggerNum]++; }
-					if(!matchedCut[2]) { eventHitCount[triggerNum]++; }
+				if(matchedCut != null) {
+					for(int cutIndex = 0; cutIndex < matchedCut.length; cutIndex++) {
+						if(!matchedCut[cutIndex]) { triggerComp[cutIndex][triggerNum]++; }
+					}
 				}
 				
 				// If there was no match found, it means that there were
 				// no triggers that were both unmatched and at the same
 				// time as this simulated trigger.
 				else {
-					eventTime[triggerNum]++;
-					singlesInternalFail = true;
+					triggerComp[TIME][triggerNum]++;
+					if(isSingles) { singlesInternalFail = true; }
+					else { pairInternalFail = true; }
 				}
 			}
 		}
@@ -1179,7 +1198,7 @@
 		
 		
 		// ==========================================================
-		// ==== SSP Singles Trigger Efficiency ======================
+		// ==== Trigger Efficiency ==================================
 		// ==========================================================
 		
 		// Reset the SSP matched trigger set.
@@ -1189,70 +1208,52 @@
 		println();
 		println("Recon Cluster Trigger --> SSP Reported Trigger Match Status");
 		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(SinglesTrigger<Cluster> simTrigger : reconSinglesTriggers.get(triggerNum)) {
-				
-				printf("\tTrigger %d :: %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d%n",
-						(triggerNum + 1), clusterPositionString(simTrigger.getTriggerSource()),
-						simTrigger.getStateClusterEnergyLow() ? 1 : 0,
-						simTrigger.getStateClusterEnergyHigh() ? 1 : 0,
-						simTrigger.getStateHitCount() ? 1 : 0);
-				printf("\t\tCluster Energy :: %.3f GeV; Hit Count :: %d%n", simTrigger.getTriggerSource().getEnergy(),
-						simTrigger.getTriggerSource().getCalorimeterHits().size());
-				printf("\t\tCluster Energy Cut :: [ %.3f GeV, %.3f GeV ]; Hit Count :: [ %.0f, INF )%n",
-						singlesTrigger[triggerNum].getCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_LOW),
-						singlesTrigger[triggerNum].getCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_HIGH),
-						singlesTrigger[triggerNum].getCutValue(TriggerModule.CLUSTER_HIT_COUNT_LOW));
+			for(Trigger<?> simTrigger : reconTriggerList.get(triggerNum)) {
+				
+				printf("\tTrigger %d :: %s :: %s%n", (triggerNum + 1),
+						triggerPositionString(simTrigger), simTrigger.toString());
 				
 				// Iterate over the SSP reported triggers and compare
 				// them to the reconstructed cluster simulated trigger.
 				matchLoop:
-				for(SSPTrigger sspTrigger : sspTriggers) {
-					// Only compare singles triggers.
-					if(sspTrigger instanceof SSPSinglesTrigger) {
-						// Cast the trigger.
-						SSPSinglesTrigger sspSingles = (SSPSinglesTrigger) sspTrigger;
-						
-						printf("\t\t\tTrigger %d :: %3d ns :: EClusterLow: %d; EClusterHigh %d; HitCount: %d",
-								sspSingles.isFirstTrigger() ? 1 : 2,
-								sspSingles.getTime(),
-								sspSingles.passCutEnergyMin() ? 1 : 0,
-								sspSingles.passCutEnergyMax() ? 1 : 0,
-								sspSingles.passCutHitCount() ? 1 : 0);
-						
-						// Only compare triggers if they are from the
-						// same trigger source.
-						if((triggerNum == 0 && sspSingles.isSecondTrigger())
-								|| (triggerNum == 1 && sspSingles.isFirstTrigger())) {
-							print(" [ fail; source    ]%n");
+				for(SSPNumberedTrigger sspTrigger : sspTriggers) {
+					printf("\t\t\t%s", sspTrigger.toString());
+					
+					// Only compare triggers if they are from the
+					// same trigger source.
+					if((triggerNum == 0 && sspTrigger.isSecondTrigger())
+							|| (triggerNum == 1 && sspTrigger.isFirstTrigger())) {
+						print(" [ fail; source    ]%n");
+						continue matchLoop;
+					}
+					
+					// Only compare the singles trigger if it was
+					// not already matched to another trigger.
+					if(sspTriggerSet.contains(sspTrigger)) {
+						print(" [ fail; matched   ]%n");
+						continue matchLoop;
+					}
+					
+					// Test each cut.
+					String[][] cutNames = {
+							{ "E_min", "E_max", "hit count", "null" },
+							{ "E_sum", "E_diff", "E_slope", "coplanar" }
+					};
+					int typeIndex = isSingles ? 0 : 1;
+					boolean[] matchedCuts = triggerCutMatch(simTrigger, sspTrigger);
+					for(int cutIndex = 0; cutIndex < matchedCuts.length; cutIndex++) {
+						if(!matchedCuts[cutIndex]) {
+							printf(" [ fail; %-9s ]%n", cutNames[typeIndex][cutIndex]);
 							continue matchLoop;
 						}
-						
-						// Only compare the singles trigger if it was
-						// not already matched to another trigger.
-						if(sspTriggerSet.contains(sspSingles)) {
-							print(" [ fail; matched   ]%n");
-							continue matchLoop;
-						}
-						
-						// Test each cut.
-						if(sspSingles.passCutEnergyMin() != simTrigger.getStateClusterEnergyLow()) {
-							print(" [ fail; E_min     ]%n");
-							continue matchLoop;
-						} if(sspSingles.passCutEnergyMax() != simTrigger.getStateClusterEnergyHigh()) {
-							print(" [ fail; E_max     ]%n");
-							continue matchLoop;
-						} if(sspSingles.passCutHitCount() != simTrigger.getStateHitCount()) {
-							print(" [ fail; hit count ]%n");
-							continue matchLoop;
-						}
-						
-						// If all the trigger flags match, then the
-						// triggers are a match.
-						reconTriggersMatched++;
-						sspTriggerSet.add(sspSingles);
-						print(" [ success         ]%n");
-						break matchLoop;
 					}
+					
+					// If all the trigger flags match, then the
+					// triggers are a match.
+					reconTriggersMatched++;
+					sspTriggerSet.add(sspTrigger);
+					print(" [ success         ]%n");
+					break matchLoop;
 				}
 			}
 		}
@@ -1265,8 +1266,9 @@
 		
 		// Get the number of SSP and reconstructed cluster simulated
 		// triggers.
-		int sspSimTriggers = sspSinglesTriggers.get(0).size() + sspSinglesTriggers.get(1).size();
-		int reconSimTriggers = reconSinglesTriggers.get(0).size() + reconSinglesTriggers.get(1).size();
+		int sspSimTriggers = sspTriggerList.get(0).size() + sspTriggerList.get(1).size();
+		int reconSimTriggers = reconTriggerList.get(0).size() + reconTriggerList.get(1).size();
+		int halfSimTriggers = sspSimTriggers / 2;
 		
 		// Print event statistics.
 		println();
@@ -1290,402 +1292,85 @@
 		}
 		
 		// Print the individual cut performances.
-		println();
-		int halfSimTriggers = sspSimTriggers / 2;
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-		printf("Trigger %d Individual Cut Failure Rate:%n", (triggerNum + 1));
-			if(sspSimTriggers == 0) {
-				printf("\tCluster Energy Lower Bound :: %d / %d%n", eventEnergyMin[triggerNum], halfSimTriggers);
-				printf("\tCluster Energy Upper Bound :: %d / %d%n", eventEnergyMax[triggerNum], halfSimTriggers);
-				printf("\tCluster Hit Count          :: %d / %d%n", eventHitCount[triggerNum], halfSimTriggers);
-			} else {
-				printf("\tCluster Energy Lower Bound :: %d / %d (%3.0f%%)%n",
-						eventEnergyMin[triggerNum], halfSimTriggers, (100.0 * eventEnergyMin[triggerNum] / halfSimTriggers));
-				printf("\tCluster Energy Upper Bound :: %d / %d (%3.0f%%)%n",
-						eventEnergyMax[triggerNum], halfSimTriggers, (100.0 * eventEnergyMax[triggerNum] / halfSimTriggers));
-				printf("\tCluster Hit Count          :: %d / %d (%3.0f%%)%n",
-						eventHitCount[triggerNum], halfSimTriggers, (100.0 * eventHitCount[triggerNum] / halfSimTriggers));
-			}
-			printf("\tExcess Reported Triggers   :: %d%n", sspReportedExtras / 2);
-		}
-		
-		// Update the global trigger tracking variables.
-		singlesSSPTriggers += sspSimTriggers;
-		singlesReconMatched += reconTriggersMatched;
-		singlesReconTriggers += reconSimTriggers;
-		singlesInternalMatched += sspInternalMatched;
-		singlesReportedTriggers += sspReportedTriggers;
-		singlesReportedExtras += sspReportedExtras;
-		
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			globalEnergyMinCut[triggerNum] += eventEnergyMin[triggerNum];
-			globalEnergyMaxCut[triggerNum] += eventEnergyMax[triggerNum];
-			globalHitCountCut[triggerNum] += eventHitCount[triggerNum];
-			globalSinglesTimeCut[triggerNum] += eventTime[triggerNum];
-		}
-		
-		// Note whether the was a singles trigger match failure.
+		if(isSingles) {
+			println();
+			for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
+			printf("Trigger %d Individual Cut Failure Rate:%n", (triggerNum + 1));
+				if(sspSimTriggers == 0) {
+					printf("\tCluster Energy Lower Bound :: %d / %d%n", triggerComp[ENERGY_MIN][triggerNum], halfSimTriggers);
+					printf("\tCluster Energy Upper Bound :: %d / %d%n", triggerComp[ENERGY_MAX][triggerNum], halfSimTriggers);
+					printf("\tCluster Hit Count          :: %d / %d%n", triggerComp[HIT_COUNT][triggerNum], halfSimTriggers);
+				} else {
+					printf("\tCluster Energy Lower Bound :: %d / %d (%3.0f%%)%n",
+							triggerComp[ENERGY_MIN][triggerNum], halfSimTriggers, (100.0 * triggerComp[ENERGY_MIN][triggerNum] / halfSimTriggers));
+					printf("\tCluster Energy Upper Bound :: %d / %d (%3.0f%%)%n",
+							triggerComp[ENERGY_MAX][triggerNum], halfSimTriggers, (100.0 * triggerComp[ENERGY_MAX][triggerNum] / halfSimTriggers));
+					printf("\tCluster Hit Count          :: %d / %d (%3.0f%%)%n",
+							triggerComp[HIT_COUNT][triggerNum], halfSimTriggers, (100.0 * triggerComp[HIT_COUNT][triggerNum] / halfSimTriggers));
+				}
+				printf("\tExcess Reported Triggers   :: %d%n", sspReportedExtras / 2);
+			}
+			
+			// Update the global trigger tracking variables.
+			singlesSSPTriggers += sspSimTriggers;
+			singlesReconMatched += reconTriggersMatched;
+			singlesReconTriggers += reconSimTriggers;
+			singlesInternalMatched += sspInternalMatched;
+			singlesReportedTriggers += sspReportedTriggers;
+			singlesReportedExtras += sspReportedExtras;
+			
+			for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
+				globalEnergyMinCut[triggerNum] += triggerComp[ENERGY_MIN][triggerNum];
+				globalEnergyMaxCut[triggerNum] += triggerComp[ENERGY_MAX][triggerNum];
+				globalHitCountCut[triggerNum] += triggerComp[HIT_COUNT][triggerNum];
+				globalSinglesTimeCut[triggerNum] += triggerComp[TIME][triggerNum];
+			}
+		} else {
+			for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
+				println();
+				printf("Trigger %d Individual Cut Failure Rate:%n", (triggerNum + 1));
+				if(sspSimTriggers == 0) {
+					printf("\tPair Energy Sum            :: %d / %d%n", triggerComp[ENERGY_SUM][triggerNum], halfSimTriggers);
+					printf("\tPair Energy Difference     :: %d / %d%n", triggerComp[ENERGY_DIFF][triggerNum], halfSimTriggers);
+					printf("\tPair Energy Slope          :: %d / %d%n", triggerComp[ENERGY_SLOPE][triggerNum], halfSimTriggers);
+					printf("\tPair Coplanarity           :: %d / %d%n", triggerComp[COPLANARITY][triggerNum], halfSimTriggers);
+					printf("\tPair Trigger Time          :: %d / %d%n", triggerComp[TIME][triggerNum], halfSimTriggers);
+				} else {
+					printf("\tPair Energy Sum            :: %d / %d (%3.0f%%)%n",
+							triggerComp[ENERGY_SUM][triggerNum], halfSimTriggers, (100.0 * triggerComp[ENERGY_SUM][triggerNum] / halfSimTriggers));
+					printf("\tPair Energy Difference     :: %d / %d (%3.0f%%)%n",
+							triggerComp[ENERGY_DIFF][triggerNum], halfSimTriggers, (100.0 * triggerComp[ENERGY_DIFF][triggerNum] / halfSimTriggers));
+					printf("\tPair Energy Slope          :: %d / %d (%3.0f%%)%n",
+							triggerComp[ENERGY_SLOPE][triggerNum], halfSimTriggers, (100.0 * triggerComp[ENERGY_SLOPE][triggerNum] / halfSimTriggers));
+					printf("\tPair Coplanarity           :: %d / %d (%3.0f%%)%n",
+							triggerComp[COPLANARITY][triggerNum], halfSimTriggers, (100.0 * triggerComp[COPLANARITY][triggerNum] / halfSimTriggers));
+					printf("\tPair Trigger Time          :: %d / %d (%3.0f%%)%n",
+							triggerComp[TIME][triggerNum], halfSimTriggers, (100.0 * triggerComp[TIME][triggerNum] / halfSimTriggers));
+				}
+				printf("\tExcess Reported Triggers   :: %d%n", sspReportedExtras / 2);
+			}
+			
+			// Update the global trigger tracking variables.
+			pairSSPTriggers += sspSimTriggers;
+			pairReconMatched += reconTriggersMatched;
+			pairReconTriggers += reconSimTriggers;
+			pairInternalMatched += sspInternalMatched;
+			pairReportedTriggers += sspReportedTriggers;
+			pairReportedExtras += sspReportedExtras;
+			
+			for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
+				globalEnergySumCut[triggerNum] += triggerComp[ENERGY_SUM][triggerNum];
+				globalEnergyDiffCut[triggerNum] += triggerComp[ENERGY_DIFF][triggerNum];
+				globalEnergySlopeCut[triggerNum] += triggerComp[ENERGY_SLOPE][triggerNum];
+				globalCoplanarityCut[triggerNum] += triggerComp[COPLANARITY][triggerNum];
+				globalPairTimeCut[triggerNum] += triggerComp[TIME][triggerNum];
+			}
+		}
+		
+		// Note whether the was a trigger match failure.
 		if((reconTriggersMatched - reconSimTriggers != 0) || (sspInternalMatched - sspSimTriggers != 0)) {
-			singlesEfficiencyFail = true;
-		}
-	}
-	
-	/**
-	 * Checks triggers simulated on SSP clusters against the SSP bank's
-	 * reported triggers to verify that the trigger is correctly applying
-	 * cuts to the clusters it sees. Additionally compares triggers
-	 * simulated on reconstructed clusters to measure trigger efficiency.
-	 */
-	private void pairTriggerVerification() {
-		// ==========================================================
-		// ==== Initialize Pair Trigger Verification ===============
-		// ==========================================================
-		
-		// Print the cluster verification header.
-		println();
-		println();
-		println("======================================================================");
-		println("=== Pair Trigger Verification ========================================");
-		println("======================================================================");
-		
-		// Track the number of triggers seen and the number found.
-		int sspReportedTriggers = 0;
-		int sspInternalMatched = 0;
-		int reconTriggersMatched = 0;
-		int sspReportedExtras = 0;
-		
-		int[] eventEnergySum = new int[2];
-		int[] eventEnergyDiff = new int[2];
-		int[] eventEnergySlope = new int[2];
-		int[] eventCoplanarity = new int[2];
-		int[] eventTime = new int[2];
-		
-		
-		
-		// ==========================================================
-		// ==== Output Event Summary ================================
-		// ==========================================================
-		
-		// Get the list of triggers reported by the SSP.
-		List<SSPPairTrigger> sspTriggers = sspBank.getPairTriggers();
-		
-		// Output the SSP cluster pair triggers.
-		println();
-		println("SSP Cluster Pair Triggers");
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(PairTrigger<SSPCluster[]> simTrigger : sspPairsTriggers.get(triggerNum)) {
-				printf("\tTrigger %d :: %s, %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d; ESumLow: %d, ESumHigh: %d, EDiff: %d, ESlope: %d, Coplanarity: %d%n",
-						(triggerNum + 1), clusterPositionString(simTrigger.getTriggerSource()[0]),
-						clusterPositionString(simTrigger.getTriggerSource()[1]),
-						simTrigger.getStateClusterEnergyLow() ? 1 : 0,
-						simTrigger.getStateClusterEnergyHigh() ? 1 : 0,
-						simTrigger.getStateHitCount() ? 1 : 0,
-						simTrigger.getStateEnergySumLow() ? 1 : 0,
-						simTrigger.getStateEnergySumHigh() ? 1 : 0,
-						simTrigger.getStateEnergyDifference() ? 1 : 0,
-						simTrigger.getStateEnergySlope() ? 1 : 0,
-						simTrigger.getStateCoplanarity() ? 1 : 0);
-			}
-		}
-		if(sspPairsTriggers.get(0).size() + sspPairsTriggers.get(1).size() == 0) {
-			println("\tNone");
-		}
-		
-		// Output the reconstructed cluster singles triggers.
-		println("Reconstructed Cluster Pair Triggers");
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(PairTrigger<Cluster[]> simTrigger : reconPairsTriggers.get(triggerNum)) {
-				printf("\tTrigger %d :: %s, %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d; ESumLow: %d, ESumHigh: %d, EDiff: %d, ESlope: %d, Coplanarity: %d%n",
-						(triggerNum + 1), clusterPositionString(simTrigger.getTriggerSource()[0]),
-						clusterPositionString(simTrigger.getTriggerSource()[1]),
-						simTrigger.getStateClusterEnergyLow() ? 1 : 0,
-						simTrigger.getStateClusterEnergyHigh() ? 1 : 0,
-						simTrigger.getStateHitCount() ? 1 : 0,
-						simTrigger.getStateEnergySumLow() ? 1 : 0,
-						simTrigger.getStateEnergySumHigh() ? 1 : 0,
-						simTrigger.getStateEnergyDifference() ? 1 : 0,
-						simTrigger.getStateEnergySlope() ? 1 : 0,
-						simTrigger.getStateCoplanarity() ? 1 : 0);
-			}
-		}
-		if(reconPairsTriggers.get(0).size() + reconPairsTriggers.get(1).size() == 0) {
-			println("\tNone");
-		}
-		
-		// Output the SSP reported triggers.
-		println("SSP Reported Pair Triggers");
-		for(SSPPairTrigger sspTrigger : sspTriggers) {
-			// Increment the number of SSP cluster singles triggers.
-			sspReportedTriggers++;
-			
-			// Get the trigger properties.
-			int triggerNum = sspTrigger.isFirstTrigger() ? 1 : 2;
-			
-			// Print the trigger.
-			printf("\tTrigger %d :: %3d ns :: ESum: %d, EDiff: %d, ESlope: %d, Coplanarity: %d%n",
-					triggerNum, sspTrigger.getTime(),
-					sspTrigger.passCutEnergySum() ? 1 : 0, sspTrigger.passCutEnergyDifference() ? 1 : 0,
-					sspTrigger.passCutEnergySlope() ? 1 : 0, sspTrigger.passCutCoplanarity() ? 1 : 0);
-		}
-		if(sspReportedTriggers == 0) { println("\tNone"); }
-		
-		
-		
-		// ==========================================================
-		// ==== SSP Internal Logic Verification =====================
-		// ==========================================================
-		
-		// Track which SSP triggers have been matched to avoid matching
-		// multiple reconstructed SSP cluster triggers to the same SSP
-		// trigger.
-		Set<SSPPairTrigger> sspTriggerSet = new HashSet<SSPPairTrigger>();
-		Set<PairTrigger<SSPCluster[]>> simTriggerSet = new HashSet<PairTrigger<SSPCluster[]>>();
-		
-		// Track the number of SSP reported triggers that are found in
-		// excess of the SSP simulated triggers.
-		int extraTriggers = sspTriggers.size() - (sspPairsTriggers.get(0).size() + sspPairsTriggers.get(1).size());
-		if(extraTriggers > 0) {
-			sspReportedExtras += extraTriggers;
-			pairInternalFail = true;
-		}
-		
-		// Iterate over the triggers.
-		println();
-		println("SSP Reported Trigger --> SSP Cluster Trigger Match Status");
-		for(SSPPairTrigger sspTrigger : sspTriggers) {
-			// Get the trigger information.
-			int triggerNum = sspTrigger.isFirstTrigger() ? 0 : 1;
-			boolean matchedTrigger = false;
-			
-			// Iterate over the SSP cluster simulated triggers and
-			// look for a trigger that matches.
-			matchLoop:
-			for(PairTrigger<SSPCluster[]> simTrigger : sspPairsTriggers.get(triggerNum)) {
-				// If the current SSP trigger has already been
-				// matched, skip it.
-				if(sspTriggerSet.contains(sspTrigger)) { continue matchLoop; }
-				
-				// Otherwise, check whether the reconstructed SSP
-				// cluster trigger matches the SSP trigger.
-				if(compareSSPPairTriggers(sspTrigger, simTrigger)) {
-					matchedTrigger = true;
-					sspTriggerSet.add(sspTrigger);
-					simTriggerSet.add(simTrigger);
-					sspInternalMatched++;
-					break matchLoop;
-				}
-				
-				printf("\tTrigger %d :: %3d ns :: ESum: %d, EDiff: %d, ESlope: %d, Coplanarity: %d :: Matched: %5b%n",
-						triggerNum, sspTrigger.getTime(), sspTrigger.passCutEnergySum() ? 1 : 0,
-						sspTrigger.passCutEnergyDifference() ? 1 : 0, sspTrigger.passCutEnergySlope() ? 1 : 0,
-						sspTrigger.passCutCoplanarity() ? 1 : 0, matchedTrigger);
-			}
-		}
-		
-		// Iterate over the unmatched simulated triggers again and the
-		// unmatched SSP reported trigger that most closely matches it.
-		simLoop:
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(PairTrigger<SSPCluster[]> simTrigger : sspPairsTriggers.get(triggerNum)) {
-				// Check whether this trigger has already been matched
-				// or not. If it has been matched, skip it.
-				if(simTriggerSet.contains(simTrigger)) { continue simLoop; }
-				
-				// Get the trigger time.
-				int simTime = 0;
-				if(simTrigger.getTriggerSource()[0].getYIndex() < 0) {
-					simTime = simTrigger.getTriggerSource()[0].getTime();
-				} else { simTime = simTrigger.getTriggerSource()[1].getTime(); }
-				
-				// Track the match statistics for each reported trigger
-				// so that the closest match may be found.
-				int numMatched = -1;
-				boolean foundBest = false;
-				boolean[] matchedCut = new boolean[4];
-				
-				// Iterate over the reported triggers to find a match.
-				reportedLoop:
-				for(SSPPairTrigger sspTrigger : sspTriggers) {
-					// If the two triggers have different times, this
-					// trigger should be skipped.
-					if(sspTrigger.getTime() != simTime) { continue reportedLoop; }
-					
-					// If this reported trigger has been matched then
-					// it should be skipped.
-					if(sspTriggerSet.contains(sspTrigger)) { continue reportedLoop; }
-					
-					// Check each of the cuts.
-					boolean[] tempMatchedCut = new boolean[4];
-					tempMatchedCut[0] = (simTrigger.getStateEnergySum()        == sspTrigger.passCutEnergySum());
-					tempMatchedCut[1] = (simTrigger.getStateEnergyDifference() == sspTrigger.passCutEnergyDifference());
-					tempMatchedCut[2] = (simTrigger.getStateEnergySlope()      == sspTrigger.passCutEnergySlope());
-					tempMatchedCut[3] = (simTrigger.getStateCoplanarity()      == sspTrigger.passCutCoplanarity());
-					
-					// Check each cut and see if this is a closer match
-					// than the previous best match.
-					int tempNumMatched = 0;
-					for(boolean passed : tempMatchedCut) { if(passed) { tempNumMatched++; } }
-					
-					// If the number of matched cuts exceeds the old
-					// best result, this becomes the new best result.
-					if(tempNumMatched > numMatched) {
-						foundBest = true;
-						numMatched = tempNumMatched;
-						matchedCut = tempMatchedCut;
-					}
-				}
-				
-				// If some match was found, note what caused it to not
-				// qualify as a complete match.
-				if(foundBest) {
-					if(!matchedCut[0]) { eventEnergySum[triggerNum]++; }
-					if(!matchedCut[1]) { eventEnergyDiff[triggerNum]++; }
-					if(!matchedCut[2]) { eventEnergySlope[triggerNum]++; }
-					if(!matchedCut[3]) { eventCoplanarity[triggerNum]++; }
-				}
-				
-				// If there was no match found, it means that there were
-				// no triggers that were both unmatched and at the same
-				// time as this simulated trigger.
-				else {
-					eventTime[triggerNum]++;
-					pairInternalFail = true;
-				}
-			}
-		}
-		
-		
-		
-		// ==========================================================
-		// ==== SSP Pair Trigger Efficiency =========================
-		// ==========================================================
-		
-		// Reset the SSP matched trigger set.
-		sspTriggerSet.clear();
-		
-		// Iterate over the reconstructed cluster pair triggers.
-		println();
-		println("Recon Cluster Trigger --> SSP Reported Trigger Match Status");
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			for(PairTrigger<Cluster[]> simTrigger : reconPairsTriggers.get(triggerNum)) {
-				// Track whether the trigger was matched.
-				boolean matchedTrigger = false;
-				
-				// Iterate over the SSP reported triggers and compare
-				// them to the reconstructed cluster simulated trigger.
-				matchLoop:
-				for(SSPTrigger sspTrigger : sspTriggers) {
-					// Only compare pair triggers.
-					if(sspTrigger instanceof SSPPairTrigger) {
-						// Cast the trigger.
-						SSPPairTrigger sspPair = (SSPPairTrigger) sspTrigger;
-						
-						// Only compare the pair trigger if it was
-						// not already matched to another trigger.
-						if(sspTriggerSet.contains(sspPair)) { continue matchLoop; }
-						
-						// Compare the triggers.
-						if(compareReconPairTriggers(sspPair, simTrigger)) {
-							reconTriggersMatched++;
-							matchedTrigger = true;
-							sspTriggerSet.add(sspPair);
-							break matchLoop;
-						}
-					}
-				}
-				
-				// Print the trigger matching status.
-				printf("\tTrigger %d :: %s, %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d; ESumLow: %d, ESumHigh: %d, EDiff: %d, ESlope: %d, Coplanarity: %d :: Matched: %5b%n",
-						(triggerNum + 1), clusterPositionString(simTrigger.getTriggerSource()[0]),
-						clusterPositionString(simTrigger.getTriggerSource()[1]),
-						simTrigger.getStateClusterEnergyLow() ? 1 : 0,
-						simTrigger.getStateClusterEnergyHigh() ? 1 : 0,
-						simTrigger.getStateHitCount() ? 1 : 0,
-						simTrigger.getStateEnergySumLow() ? 1 : 0,
-						simTrigger.getStateEnergySumHigh() ? 1 : 0,
-						simTrigger.getStateEnergyDifference() ? 1 : 0,
-						simTrigger.getStateEnergySlope() ? 1 : 0,
-						simTrigger.getStateCoplanarity() ? 1 : 0, matchedTrigger);
-			}
-		}
-		
-		
-		
-		// ==========================================================
-		// ==== Output Event Results ================================
-		// ==========================================================
-		
-		// Get the number of SSP and reconstructed cluster simulated
-		// triggers.
-		int sspSimTriggers = sspPairsTriggers.get(0).size() + sspPairsTriggers.get(1).size();
-		int reconSimTriggers = reconPairsTriggers.get(0).size() + reconPairsTriggers.get(1).size();
-		int halfSimTriggers = sspSimTriggers / 2;
-		
-		// Print event statistics.
-		println();
-		println("Event Statistics:");
-		printf("\tSSP Cluster Sim Triggers   :: %d%n", sspSimTriggers);
-		printf("\tRecon Cluster Sim Triggers :: %d%n", reconSimTriggers);
-		printf("\tSSP Reported Triggers      :: %d%n", sspReportedTriggers);
-		if(sspSimTriggers == 0) {
-			printf("\tInternal Efficiency        :: %d / %d (N/A)%n",
-					sspInternalMatched, sspSimTriggers);
-		} else {
-			printf("\tInternal Efficiency        :: %d / %d (%3.0f%%)%n",
-					sspInternalMatched, sspSimTriggers, (100.0 * sspInternalMatched / sspSimTriggers));
-		}
-		if(reconSimTriggers == 0) {
-			printf("\tTrigger Efficiency         :: %d / %d (N/A)%n",
-					reconTriggersMatched, reconSimTriggers);
-		} else {
-			printf("\tTrigger Efficiency         :: %d / %d (%3.0f%%)%n",
-					reconTriggersMatched, reconSimTriggers, (100.0 * reconTriggersMatched / reconSimTriggers));
-		}
-		
-		// Print the individual cut performances.
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			println();
-			printf("Trigger %d Individual Cut Failure Rate:%n", (triggerNum + 1));
-			if(sspSimTriggers == 0) {
-				printf("\tPair Energy Sum            :: %d / %d%n", eventEnergySum[triggerNum], halfSimTriggers);
-				printf("\tPair Energy Difference     :: %d / %d%n", eventEnergyDiff[triggerNum], halfSimTriggers);
-				printf("\tPair Energy Slope          :: %d / %d%n", eventEnergySlope[triggerNum], halfSimTriggers);
-				printf("\tPair Coplanarity           :: %d / %d%n", eventCoplanarity[triggerNum], halfSimTriggers);
-				printf("\tPair Trigger Time          :: %d / %d%n", eventTime[triggerNum], halfSimTriggers);
-			} else {
-				printf("\tPair Energy Sum            :: %d / %d (%3.0f%%)%n",
-						eventEnergySum[triggerNum], halfSimTriggers, (100.0 * eventEnergySum[triggerNum] / halfSimTriggers));
-				printf("\tPair Energy Difference     :: %d / %d (%3.0f%%)%n",
-						eventEnergyDiff[triggerNum], halfSimTriggers, (100.0 * eventEnergyDiff[triggerNum] / halfSimTriggers));
-				printf("\tPair Energy Slope          :: %d / %d (%3.0f%%)%n",
-						eventEnergySlope[triggerNum], halfSimTriggers, (100.0 * eventEnergySlope[triggerNum] / halfSimTriggers));
-				printf("\tPair Coplanarity           :: %d / %d (%3.0f%%)%n",
-						eventCoplanarity[triggerNum], halfSimTriggers, (100.0 * eventCoplanarity[triggerNum] / halfSimTriggers));
-				printf("\tPair Trigger Time          :: %d / %d (%3.0f%%)%n",
-						eventTime[triggerNum], halfSimTriggers, (100.0 * eventTime[triggerNum] / halfSimTriggers));
-			}
-			printf("\tExcess Reported Triggers   :: %d%n", sspReportedExtras / 2);
-		}
-		
-		// Update the global trigger tracking variables.
-		pairSSPTriggers += sspSimTriggers;
-		pairReconMatched += reconTriggersMatched;
-		pairReconTriggers += reconSimTriggers;
-		pairInternalMatched += sspInternalMatched;
-		pairReportedTriggers += sspReportedTriggers;
-		pairReportedExtras += sspReportedExtras;
-		
-		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			globalEnergySumCut[triggerNum] += eventEnergySum[triggerNum];
-			globalEnergyDiffCut[triggerNum] += eventEnergyDiff[triggerNum];
-			globalEnergySlopeCut[triggerNum] += eventEnergySlope[triggerNum];
-			globalCoplanarityCut[triggerNum] += eventCoplanarity[triggerNum];
-			globalPairTimeCut[triggerNum] += eventTime[triggerNum];
-		}
-		
-		// Note whether the was a singles trigger match failure.
-		if((reconTriggersMatched - reconSimTriggers != 0) || (sspInternalMatched - sspSimTriggers != 0)) {
-			pairEfficiencyFail = true;
+			if(isSingles) { singlesEfficiencyFail = true; }
+			else { pairEfficiencyFail = true; }
 		}
 	}
 	
@@ -2031,6 +1716,41 @@
 	}
 	
 	/**
+	 * Compares an SSP trigger with a simulated trigger. Note that only
+	 * certain class combinations are supported. Triggers of the type
+	 * <code>SSPSinglesTrigger</code> may be compared with triggers of
+	 * the type <code>SinglesTrigger<SSPCluster></code> and triggers of
+	 * the type <code>SSPPairTrigger</code> may be compared to either
+	 * <code>PairTrigger<SSPCluster[]></code> triggers objects.
+	 * @param bankTrigger - The SSP bank trigger.
+	 * @param simTrigger - The simulated trigger.
+	 * @return Returns <code>true</code> if the triggers are valid
+	 * matches and <code>false</code> if they are not.
+	 * @throws IllegalArgumentException Occurs if the trigger types
+	 * are not of a supported type.
+	 */
+	@SuppressWarnings("unchecked")
+	private static final boolean compareTriggers(SSPNumberedTrigger bankTrigger, Trigger<?> simTrigger) throws IllegalArgumentException {
+		// Get the classes of the arguments. This is used to check the
+		// generic type of the Trigger<?> object, and means that the
+		// "unchecked" warnings can be safely ignored.
+		Object source = simTrigger.getTriggerSource();
+		
+		// If the combination of classes is supported, pass the triggers
+		// to the appropriate handler.
+		if(bankTrigger instanceof SSPSinglesTrigger && simTrigger instanceof SinglesTrigger && source instanceof SSPCluster) {
+			return compareSSPSinglesTriggers((SSPSinglesTrigger) bankTrigger, (SinglesTrigger<SSPCluster>) simTrigger);
+		} else if(bankTrigger instanceof SSPPairTrigger && simTrigger instanceof PairTrigger && source instanceof SSPCluster[]) {
+			return compareSSPPairTriggers((SSPPairTrigger) bankTrigger, (PairTrigger<SSPCluster[]>) simTrigger);
+		}
+		
+		// Otherwise, the trigger combination is not supported. Produce
+		// and exception.
+		throw new IllegalArgumentException(String.format("Trigger type \"%s\" can not be compared to trigger type \"%s\" with source type \"%s\".",
+				bankTrigger.getClass().getSimpleName(), simTrigger.getClass().getSimpleName(), source.getClass().getSimpleName()));
+	}
+	
+	/**
 	 * Compares a trigger from the SSP bank to a trigger simulated on
 	 * an SSP cluster.
 	 * @param bankTrigger - The trigger from the SSP bank.
@@ -2088,40 +1808,6 @@
 		
 		// If the time stamp is the same, check that the trigger flags
 		// are all the same. Start with energy sum.
-		if(bankTrigger.passCutEnergySum() != simTrigger.getStateEnergySum()) {
-			return false;
-		}
-		
-		// Check pair energy difference.
-		if(bankTrigger.passCutEnergyDifference() != simTrigger.getStateEnergyDifference()) {
-			return false;
-		}
-		
-		// Check pair energy slope.
-		if(bankTrigger.passCutEnergySlope() != simTrigger.getStateEnergySlope()) {
-			return false;
-		}
-		
-		// Check pair coplanarity.
-		if(bankTrigger.passCutCoplanarity() != simTrigger.getStateCoplanarity()) {
-			return false;
-		}
-		
-		// If all of the tests are successful, the triggers match.
-		return true;
-	}
-	
-	/**
-	 * Compares a trigger from the SSP bank to a trigger simulated on
-	 * an reconstructed cluster.
-	 * @param bankTrigger - The trigger from the SSP bank.
-	 * @param simTrigger - The trigger from the simulation.
-	 * @return Returns <code>true</code> if the triggers match and
-	 * <code>false</code> if they do not.
-	 */
-	private static final boolean compareReconPairTriggers(SSPPairTrigger bankTrigger, PairTrigger<Cluster[]> simTrigger) {
-		// Check that the trigger flags are all the same. Start with
-		// energy sum.
 		if(bankTrigger.passCutEnergySum() != simTrigger.getStateEnergySum()) {
 			return false;
 		}
@@ -2301,6 +1987,138 @@
 	}
 	
 	/**
+	 * Gets the position of the source of a <code>Trigger</code> object
+	 * as text. This method only supports trigger sources of the types
+	 * <code>SSPCluster</code>, <code>Cluster</code>, and arrays of size
+	 * two of either type.
+	 * @param trigger - The trigger from which to obtain the source.
+	 * @return Returns the source of the trigger as a <code>String</code>
+	 * object.
+	 * @throws IllegalArgumentException Occurs if the source of the
+	 * trigger is not any of the supported types.
+	 */
+	private static final String triggerPositionString(Trigger<?> trigger) throws IllegalArgumentException {
+		// Get the trigger source.
+		Object source = trigger.getTriggerSource();
+		
+		// Handle valid trigger sources.
+		if(source instanceof SSPCluster) {
+			return clusterPositionString((SSPCluster) source);
+		} else if(source instanceof Cluster) {
+			return clusterPositionString((Cluster) source);
+		} else if(source instanceof SSPCluster[]) {
+			SSPCluster[] sourcePair = (SSPCluster[]) source;
+			if(sourcePair.length == 2) {
+				return String.format("%s, %s", clusterPositionString(sourcePair[0]),
+						clusterPositionString(sourcePair[1]));
+			}
+		} else if(source instanceof Cluster[]) {
+			Cluster[] sourcePair = (Cluster[]) source;
+			if(sourcePair.length == 2) {
+				return String.format("%s, %s", clusterPositionString(sourcePair[0]),
+						clusterPositionString(sourcePair[1]));
+			}
+		}
+		
+		// Otherwise, the source type is unrecognized. Throw an error.
+		throw new IllegalArgumentException(String.format("Trigger source type \"%s\" is not supported.",
+				trigger.getTriggerSource().getClass().getSimpleName()));
+	}
+	
+	/**
+	 * Gets the time of a simulated trigger object. Method supports
+	 * triggers with source objects of type <code>SSPCluster</code>,
+	 * <code>Cluster</code>, and arrays of size two composed of either
+	 * object type.
+	 * @param trigger - The trigger.
+	 * @return Returns the time at which the trigger occurred.
+	 * @throws IllegalArgumentException Occurs if the trigger source
+	 * is not a supported type.
+	 */
+	private static final double getTriggerTime(Trigger<?> trigger) throws IllegalArgumentException {
+		// Get the trigger source.
+		Object source = trigger.getTriggerSource();
+		
+		// Get the trigger time for supported trigger types.
+		if(source instanceof SSPCluster) {
+			return ((SSPCluster) source).getTime();
+		} else if(source instanceof Cluster) {
+			return TriggerDiagnosticUtil.getClusterTime((Cluster) source);
+		} else if(source instanceof SSPCluster[]) {
+			// Get the pair.
+			SSPCluster[] sourcePair = (SSPCluster[]) source;
+			
+			// Get the time of the bottom cluster.
+			if(sourcePair.length == 2) {
+				if(sourcePair[0].getYIndex() < 0) { return sourcePair[0].getTime(); }
+				else if(sourcePair[1].getYIndex() < 0) { return sourcePair[1].getTime(); }
+				else { throw new IllegalArgumentException("Cluster pairs must be formed of a top/bottom pair."); }
+			}
+			else { throw new IllegalArgumentException("Cluster pairs must be of size 2."); }
+		} else if(source instanceof Cluster[]) {
+			// Get the pair.
+			Cluster[] sourcePair = (Cluster[]) source;
+			int[] iy = {
+				TriggerDiagnosticUtil.getYIndex(sourcePair[0]),
+				TriggerDiagnosticUtil.getYIndex(sourcePair[1])
+			};
+			
+			// Get the time of the bottom cluster.
+			if(sourcePair.length == 2) {
+				if(iy[0] < 0) { return TriggerDiagnosticUtil.getClusterTime(sourcePair[0]); }
+				else if(iy[1] < 0) { return TriggerDiagnosticUtil.getClusterTime(sourcePair[1]); }
+				else { throw new IllegalArgumentException("Cluster pairs must be formed of a top/bottom pair."); }
+			}
+			else { throw new IllegalArgumentException("Cluster pairs must be of size 2."); }
+		}
+		
+		// If the source type is unrecognized, throw an exception.
+		throw new IllegalArgumentException(String.format("Trigger source type \"%\" is not supported.",
+				source.getClass().getSimpleName()));
+	}
+	
+	private static final boolean[] triggerCutMatch(Trigger<?> simTrigger, SSPTrigger sspTrigger) {
+		// Check that the cuts match for supported trigger types.
+		if(simTrigger instanceof SinglesTrigger && sspTrigger instanceof SSPSinglesTrigger) {
+			// Create an array to store the cut checks.
+			boolean[] cutMatch = new boolean[3];
+			
+			// Cast the triggers.
+			SinglesTrigger<?> simSingles = (SinglesTrigger<?>) simTrigger;
+			SSPSinglesTrigger sspSingles = (SSPSinglesTrigger) sspTrigger;
+			
+			// Perform the check.
+			cutMatch[ENERGY_MIN] = (simSingles.getStateClusterEnergyLow()  == sspSingles.passCutEnergyMin());
+			cutMatch[ENERGY_MAX] = (simSingles.getStateClusterEnergyHigh() == sspSingles.passCutEnergyMax());
+			cutMatch[HIT_COUNT] = (simSingles.getStateHitCount()          == sspSingles.passCutHitCount());
+			
+			// Return the match array.
+			return cutMatch;
+		} else if(simTrigger instanceof PairTrigger && sspTrigger instanceof SSPPairTrigger) {
+			// Create an array to store the cut checks.
+			boolean[] cutMatch = new boolean[4];
+			
+			// Cast the triggers.
+			PairTrigger<?> simPair = (PairTrigger<?>) simTrigger;
+			SSPPairTrigger sspPair = (SSPPairTrigger) sspTrigger;
+			
+			// Perform the check.
+			cutMatch[ENERGY_SUM] = (simPair.getStateEnergySum()        == sspPair.passCutEnergySum());
+			cutMatch[ENERGY_DIFF] = (simPair.getStateEnergyDifference() == sspPair.passCutEnergyDifference());
+			cutMatch[ENERGY_SLOPE] = (simPair.getStateEnergySlope()      == sspPair.passCutEnergySlope());
+			cutMatch[COPLANARITY] = (simPair.getStateCoplanarity()      == sspPair.passCutCoplanarity());
+			
+			// Return the match array.
+			return cutMatch;
+		}
+		
+		// If this point is reached, the triggers are not of a supported
+		// type for cut comparison. Produce an exception.
+		throw new IllegalArgumentException(String.format("Triggers of type \"%s\" can not be cut-matched with triggers of type \"%s\".",
+				simTrigger.getClass().getSimpleName(), sspTrigger.getClass().getSimpleName()));
+	}
+	
+	/**
 	 * Class <code>Pair</code> provides a convenient means of putting
 	 * a reconstructed cluster and an SSP cluster in the same object
 	 * for cluster matching.

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticGUIDriver.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticGUIDriver.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticGUIDriver.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,43 @@
+package org.hps.users.kmccarty;
+
+import java.util.List;
+
+import javax.swing.JFrame;
+
+import org.hps.users.kmccarty.diagpanel.ClusterTablePanel;
+import org.lcsim.event.EventHeader;
+import org.lcsim.util.Driver;
+
+public class TriggerDiagnosticGUIDriver extends Driver {
+	private JFrame window = new JFrame();
+	private ClusterTablePanel clusterTable = new ClusterTablePanel();
+	private String diagnosticCollectionName = "DiagnosticSnapshot";
+	
+	@Override
+	public void startOfData() {
+		window.add(clusterTable);
+		window.setVisible(true);
+		window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+		window.setSize(500, 400);
+	}
+	
+	@Override
+	public void process(EventHeader event) {
+		// Updates are only performed if a diagnostic snapshot object
+		// exists. Otherwise, do nothing.
+		if(event.hasCollection(DiagSnapshot.class, diagnosticCollectionName)) {
+			// Get the snapshot collection.
+			List<DiagSnapshot> snapshotList = event.get(DiagSnapshot.class, diagnosticCollectionName);
+			
+			// Get the snapshot. There will only ever be one.
+			DiagSnapshot snapshot = snapshotList.get(0);
+			
+			// Feed it to the table.
+			clusterTable.updatePanel(snapshot);
+		}
+	}
+	
+	public void setDiagnosticCollectionName(String name) {
+		diagnosticCollectionName = name;
+	}
+}

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/AbstractTablePanel.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/AbstractTablePanel.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/AbstractTablePanel.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,230 @@
+package org.hps.users.kmccarty.diagpanel;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+
+/**
+ * Class <code>AbstractTablePanel</code> displays two <code>JTable</code>
+ * objects side-by-side with headers above them. The left table displays
+ * statistical data for recent events processed with trigger diagnostics
+ * while the right table displays the same, but over the course of the
+ * entire run.<br/>
+ * <br/>
+ * This implements the interface <code>DiagnosticUpdatable</code>.
+ * 
+ * @author Kyle McCarty <[log in to unmask]>
+ * @see JPanel
+ * @see DiagnosticUpdatable
+ */
+public abstract class AbstractTablePanel extends JPanel implements DiagnosticUpdatable {
+	// Static variables.
+	private static final long serialVersionUID = 0L;
+	
+	// Table models.
+	private final TableTextModel localModel;
+	private final TableTextModel globalModel;
+	
+	// Components.
+	private JTable localTable;
+	private JLabel localHeader;
+	private JTable globalTable;
+	private JLabel globalHeader;
+	private Dimension defaultPrefSize = new Dimension(0, 0);
+	private Dimension userPrefSize = null;
+	
+	// Table model mappings.
+	private static final int COL_TITLE = 0;
+	private static final int COL_VALUE = 1;
+	
+	/**
+	 * Instantiates an <code>AbstractTablePanel</code> with a number
+	 * of rows equal to the length of the argument array. Note that
+	 * the panel requires that there be at least one row.
+	 * @param rowNames - An array of <code>String</code> objects that
+	 * are to be displayed for the names of the table rows.
+	 */
+	public AbstractTablePanel(String[] rowNames) {
+		// Initialize the table models. They should have two columns
+		// (one for values and one for headers) and a number of rows
+		// equal to the number of row names.
+		localModel = new TableTextModel(rowNames.length, 2);
+		globalModel = new TableTextModel(rowNames.length, 2);
+		
+		// Initialize the titles.
+		for(int i = 0; i < rowNames.length; i++) {
+			localModel.setValueAt(rowNames[i], i, COL_TITLE);
+			globalModel.setValueAt(rowNames[i], i, COL_TITLE);
+		}
+		updatePanel(null);
+		
+		// Define the panel layout.
+		//SpringLayout layout = new SpringLayout();
+		setLayout(null);
+		
+		// Create header labels for the tables.
+		localHeader = new JLabel("Local Statistics");
+		localHeader.setHorizontalAlignment(JLabel.CENTER);
+		add(localHeader);
+		
+		globalHeader = new JLabel("Run Statistics");
+		globalHeader.setHorizontalAlignment(JLabel.CENTER);
+		add(globalHeader);
+		
+		// Create JTable objects to display the data.
+		localTable = new JTable(localModel);
+		localTable.setRowSelectionAllowed(false);
+		localTable.setColumnSelectionAllowed(false);
+		localTable.setCellSelectionEnabled(false);
+		add(localTable);
+		
+		globalTable = new JTable(globalModel);
+		globalTable.setRowSelectionAllowed(false);
+		globalTable.setColumnSelectionAllowed(false);
+		globalTable.setCellSelectionEnabled(false);
+		add(globalTable);
+		
+		// Track when the component changes size and reposition the
+		// components accordingly.
+		addComponentListener(new ComponentAdapter() {
+			@Override
+			public void componentResized(ComponentEvent e) { positionComponents(); }
+		});
+		
+		// Define the component preferred size.
+		defaultPrefSize.width = localTable.getPreferredSize().width +
+				ComponentUtils.hinternal + globalTable.getPreferredSize().width;
+		defaultPrefSize.height = localTable.getPreferredSize().height +
+				ComponentUtils.vinternal + globalTable.getPreferredSize().height;
+	}
+	
+	@Override
+	public Dimension getPreferredSize() {
+		// If there is a user-specified preferred size, return that.
+		if(userPrefSize == null) { return defaultPrefSize; }
+		
+		// Otherwise, return the default calculated preferred size.
+		else { return userPrefSize; }
+	}
+	
+	@Override
+	public void setBackground(Color bg) {
+		// Set the base component background.
+		super.setBackground(bg);
+		
+		// If the components have been initialized, pass the background
+		// color change to them as appropriate. Note that the tables
+		// will always retain the same background color.
+		if(localTable != null) {
+			// Set the header backgrounds.
+			localHeader.setBackground(bg);
+			globalHeader.setBackground(bg);
+		}
+	}
+	
+	@Override
+	public void setFont(Font font) {
+		// Set the base component font.
+		super.setFont(font);
+		
+		// If the components have been initialized, pass the font change
+		// to them as appropriate.
+		if(localTable != null) {
+			// Set the table fonts.
+			localTable.setFont(font);
+			globalTable.setFont(font);
+			
+			// Set the header fonts.
+			Font headerFont = font.deriveFont(Font.BOLD, (float) Math.ceil(font.getSize2D() * 1.3));
+			localHeader.setFont(headerFont);
+			globalHeader.setFont(headerFont);
+		}
+	}
+	
+	@Override
+	public void setForeground(Color fg) {
+		// Set the base component foreground.
+		super.setForeground(fg);
+		
+		// If the components have been initialized, pass the foreground
+		// color change to them as appropriate. Note that the tables
+		// will always retain the same foreground color.
+		if(localTable != null) {
+			// Set the header foregrounds.
+			localHeader.setForeground(fg);
+			globalHeader.setForeground(fg);
+		}
+	}
+	
+	@Override
+	public void setPreferredSize(Dimension preferredSize) {
+		userPrefSize = preferredSize;
+	}
+	
+	/**
+	 * Sets the value of the indicated row for the global statistical
+	 * table.
+	 * @param rowIndex - The row.
+	 * @param value - The new value.
+	 */
+	protected void setGlobalRowValue(int rowIndex, String value) {
+		globalModel.setValueAt(value, rowIndex, COL_VALUE);
+	}
+	
+	/**
+	 * Sets the value of the indicated row for the local statistical
+	 * table.
+	 * @param rowIndex - The row.
+	 * @param value - The new value.
+	 */
+	protected void setLocalRowValue(int rowIndex, String value) {
+		localModel.setValueAt(value, rowIndex, COL_VALUE);
+	}
+	
+	/**
+	 * Repositions the components to the correct places on the parent
+	 * <code>JPanel</code>. This should be run whenever the panel
+	 * changes size.
+	 */
+	private void positionComponents() {
+		// Do not update if the components have not been initialized.
+		if(localHeader == null) { return; }
+		
+		// The local components get the left half of the panel and the
+		// global components the right. Find half of the panel width,
+		// accounting for the internal spacing. This is an internal
+		// component, so it does not employ additional spacing between
+		// itself and the parent component's edges.
+		int compWidth = (getWidth() - 10) / 2;
+		
+		// If there is any width remaining, it goes to the spacing.
+		int horizontal = ComponentUtils.hinternal + (getWidth() - 10) % 2;
+		
+		// Place the header labels. These are given their preferred
+		// height. Note that this means a very small panel may cut off
+		// some of the components. First, get the preferred height of
+		// the label with the larger preferred height. These should be
+		// the same thing, but just in case...
+		int labelHeight = localHeader.getPreferredSize().height;
+		if(labelHeight < globalHeader.getPreferredSize().height) {
+			labelHeight = globalHeader.getPreferredSize().height;
+		}
+		
+		// Set the label sizes and positions.
+		localHeader.setBounds(0, 0, compWidth, labelHeight);
+		globalHeader.setLocation(ComponentUtils.getNextX(localHeader, horizontal), 0);
+		globalHeader.setSize(compWidth, labelHeight);
+		
+		// The tables go under their respective labels and should fill
+		// the remainder of the label height.
+		int tableY = ComponentUtils.getNextY(localHeader, ComponentUtils.vinternal);
+		localTable.setBounds(0, tableY, compWidth, localTable.getPreferredSize().height);
+		globalTable.setBounds(globalHeader.getX(), tableY, compWidth, globalTable.getPreferredSize().height);
+	}
+}

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ClusterTablePanel.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ClusterTablePanel.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ClusterTablePanel.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,111 @@
+package org.hps.users.kmccarty.diagpanel;
+
+import org.hps.users.kmccarty.DiagSnapshot;
+
+/**
+ * Class <code>ClusterTablePanel</code> is an implementation of class
+ * <code>AbstractTablePanel</code> for cluster statistical data.<br/>
+ * <br/>
+ * This implements the interface <code>DiagnosticUpdatable</code>.
+ * 
+ * @author Kyle McCarty <[log in to unmask]>
+ * @see AbstractTablePanel
+ */
+public class ClusterTablePanel extends AbstractTablePanel {
+	// Static variables.
+	private static final long serialVersionUID = 0L;
+	private static final String[] TABLE_TITLES = { "Recon Clusters:", "SSP Clusters", "Matched Clusters",
+			"Failed (Position)", "Failed (Energy)", "Failed (Hit Count)" };
+	
+	// Table model mappings.
+	private static final int ROW_RECON_COUNT      = 0;
+	private static final int ROW_SSP_COUNT        = 1;
+	private static final int ROW_MATCHED          = 2;
+	private static final int ROW_FAILED_POSITION  = 3;
+	private static final int ROW_FAILED_ENERGY    = 4;
+	private static final int ROW_FAILED_HIT_COUNT = 5;
+	
+	/**
+	 * Instantiate a new <code>ClusterTablePanel</code>.
+	 */
+	public ClusterTablePanel() { super(TABLE_TITLES); }
+	
+	@Override
+	public void updatePanel(DiagSnapshot snapshot) {
+		// If the snapshot is null, all values should be "N/A."
+		if(snapshot == null) {
+			// Output cluster count data.
+			String scalerNullValue = "---";
+			setLocalRowValue(ROW_RECON_COUNT,  scalerNullValue);
+			setLocalRowValue(ROW_SSP_COUNT,    scalerNullValue);
+			setGlobalRowValue(ROW_RECON_COUNT, scalerNullValue);
+			setGlobalRowValue(ROW_SSP_COUNT,   scalerNullValue);
+			
+			// Output the tracked statistical data.
+			String percentNullValue = "--- / --- (---%)";
+			setLocalRowValue(ROW_MATCHED,           percentNullValue);
+			setLocalRowValue(ROW_FAILED_POSITION,   percentNullValue);
+			setLocalRowValue(ROW_FAILED_ENERGY,     percentNullValue);
+			setLocalRowValue(ROW_FAILED_HIT_COUNT,  percentNullValue);
+			setGlobalRowValue(ROW_MATCHED,          percentNullValue);
+			setGlobalRowValue(ROW_FAILED_POSITION,  percentNullValue);
+			setGlobalRowValue(ROW_FAILED_ENERGY,    percentNullValue);
+			setGlobalRowValue(ROW_FAILED_HIT_COUNT, percentNullValue);
+		}
+		
+		// Otherwise, populate the table with the diagnostic data.
+		else {
+			/*
+			 * This is disabled until the snapshot object is stable and
+			 * is subject to change. It will not work if enabled now.
+			// Get the largest number of digits in any of the values.
+			int mostDigits = 0;
+			for(int valueID = 0; valueID < DiagSnapshot.CL_BANK_SIZE; valueID++) {
+				int localDigits = ComponentUtils.getDigits(snapshot.getClusterValue(LOCAL, valueID));
+				int globalDigits = ComponentUtils.getDigits(snapshot.getClusterValue(GLOBAL, valueID));
+				mostDigits = ComponentUtils.max(mostDigits, localDigits, globalDigits);
+			}
+			
+			// Put the number of reconstructed and SSP clusters into
+			// the tables.
+			int[] clusterValue = {
+					snapshot.getClusterValue(LOCAL,  DiagSnapshot.CL_VALUE_RECON_CLUSTERS),
+					snapshot.getClusterValue(LOCAL,  DiagSnapshot.CL_VALUE_SSP_CLUSTERS),
+					snapshot.getClusterValue(GLOBAL, DiagSnapshot.CL_VALUE_RECON_CLUSTERS),
+					snapshot.getClusterValue(GLOBAL, DiagSnapshot.CL_VALUE_SSP_CLUSTERS)
+			};
+			String countFormat = "%" + mostDigits + "d";
+			setLocalRowValue(ROW_RECON_COUNT,  String.format(countFormat, clusterValue[0]));
+			setLocalRowValue(ROW_SSP_COUNT,    String.format(countFormat, clusterValue[1]));
+			setGlobalRowValue(ROW_RECON_COUNT, String.format(countFormat, clusterValue[2]));
+			setGlobalRowValue(ROW_SSP_COUNT,   String.format(countFormat, clusterValue[3]));
+			
+			// Output the tracked statistical data.
+			int total;
+			String percentFormat = "%" + mostDigits + "d / %" + mostDigits + "d (%7.3f)";
+			int[] statValue = {
+					snapshot.getClusterValue(DiagSnapshot.TYPE_LOCAL, DiagSnapshot.CL_VALUE_MATCHED),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_LOCAL, DiagSnapshot.CL_VALUE_FAIL_POSITION),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_LOCAL, DiagSnapshot.CL_VALUE_FAIL_ENERGY),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_LOCAL, DiagSnapshot.CL_VALUE_FAIL_HIT_COUNT),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_GLOBAL, DiagSnapshot.CL_VALUE_MATCHED),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_GLOBAL, DiagSnapshot.CL_VALUE_FAIL_POSITION),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_GLOBAL, DiagSnapshot.CL_VALUE_FAIL_ENERGY),
+					snapshot.getClusterValue(DiagSnapshot.TYPE_GLOBAL, DiagSnapshot.CL_VALUE_FAIL_HIT_COUNT)
+			};
+			
+			total = snapshot.getClusterValue(DiagSnapshot.TYPE_LOCAL, DiagSnapshot.CL_VALUE_RECON_CLUSTERS);
+			setLocalRowValue(ROW_MATCHED,          String.format(percentFormat, statValue[0], total, 100.0 * statValue[0] / total));
+			setLocalRowValue(ROW_FAILED_POSITION,  String.format(percentFormat, statValue[1], total, 100.0 * statValue[1] / total));
+			setLocalRowValue(ROW_FAILED_ENERGY,    String.format(percentFormat, statValue[2], total, 100.0 * statValue[2] / total));
+			setLocalRowValue(ROW_FAILED_HIT_COUNT, String.format(percentFormat, statValue[3], total, 100.0 * statValue[3] / total));
+			
+			total = snapshot.getClusterValue(DiagSnapshot.TYPE_GLOBAL, DiagSnapshot.CL_VALUE_RECON_CLUSTERS);
+			setGlobalRowValue(ROW_MATCHED,          String.format(percentFormat, statValue[4], total, 100.0 * statValue[4] / total));
+			setGlobalRowValue(ROW_FAILED_POSITION,  String.format(percentFormat, statValue[5], total, 100.0 * statValue[5] / total));
+			setGlobalRowValue(ROW_FAILED_ENERGY,    String.format(percentFormat, statValue[6], total, 100.0 * statValue[6] / total));
+			setGlobalRowValue(ROW_FAILED_HIT_COUNT, String.format(percentFormat, statValue[7], total, 100.0 * statValue[7] / total));
+			*/
+		}
+	}
+}

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ComponentUtils.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ComponentUtils.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/ComponentUtils.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,106 @@
+package org.hps.users.kmccarty.diagpanel;
+
+import java.awt.Component;
+
+import org.hps.users.kmccarty.TriggerDiagnosticUtil;
+
+/**
+ * Class <code>ComponentUtils</code> is a list of utility methods used
+ * by the trigger diagnostic GUI.
+ * 
+ * @author Kyle McCarty <[log in to unmask]>
+ */
+class ComponentUtils {
+	/** The default spacing used between a horizontal edge of one
+	 * component and the horizontal edge of another. */
+	public static final int hinternal = 10;
+	/** The default spacing used between a vertical edge of one
+	 * component and the vertical edge of another. */
+	public static final int vinternal = 10;
+	/** The default spacing used between a horizontal edge of one
+	 * component and the edge of its parent component. */
+	public static final int hexternal = 0;
+	/** The default spacing used between a vertical edge of one
+	 * component and the edge of its parent component. */
+	public static final int vexternal = 0;
+	
+	/**
+	 * Gets the number of digits in the base-10 String representation
+	 * of an integer primitive. Negative signs are not included in the
+	 * digit count.
+	 * @param value - The value of which to obtain the length.
+	 * @return Returns the number of digits in the String representation
+	 * of the argument value.
+	 */
+	public static final int getDigits(int value) {
+		return TriggerDiagnosticUtil.getDigits(value);
+	}
+	
+	/**
+	 * Gets the x-coordinate immediately to the right of the given
+	 * component.
+	 * @param c - The component of which to find the edge.
+	 * @return Returns the x-coordinate as an <code>int</code> value.
+	 */
+	static final int getNextX(Component c) {
+		return getNextX(c, 0);
+	}
+	
+	/**
+	 * Gets the x-coordinate a given distance to the right edge of the
+	 * argument component.
+	 * @param c - The component of which to find the edge.
+	 * @param spacing - The additional spacing past the edge of the
+	 * component to add.
+	 * @return Returns the x-coordinate as an <code>int</code> value.
+	 */
+	static final int getNextX(Component c, int spacing) {
+		return c.getX() + c.getWidth() + spacing;
+	}
+	
+	/**
+	 * Gets the y-coordinate immediately below the given component.
+	 * @param c - The component of which to find the edge.
+	 * @return Returns the y-coordinate as an <code>int</code> value.
+	 */
+	static final int getNextY(Component c) {
+		return getNextY(c, 0);
+	}
+	
+	/**
+	 * Gets the y-coordinate a given distance below the bottom edge
+	 * of the argument component.
+	 * @param c - The component of which to find the edge.
+	 * @param spacing - The additional spacing past the edge of the
+	 * component to add.
+	 * @return Returns the y-coordinate as an <code>int</code> value.
+	 */
+	static final int getNextY(Component c, int spacing) {
+		return c.getY() + c.getHeight() + spacing;
+	}
+	
+	/**
+	 * Gets the maximum value from a list of values.
+	 * @param values - The values to compare.
+	 * @return Returns the largest of the argument values.
+	 * @throws IllegalArgumentException Occurs if no values are given.
+	 */
+	public static final int max(int... values) throws IllegalArgumentException {
+		// Throw an error if no arguments are provided.
+		if(values == null || values.length == 0) {
+			throw new IllegalArgumentException("Can not determine maximum value from a list of 0 values.");
+		}
+		
+		// If there is only one value, return it.
+		if(values.length == 1) { return values[0]; }
+		
+		// Otherwise, get the largest value.
+		int largest = Integer.MIN_VALUE;
+		for(int value : values) {
+			if(value > largest) { largest = value; }
+		}
+		
+		// Return the result.
+		return largest;
+	}
+}

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/DiagnosticUpdatable.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/DiagnosticUpdatable.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/DiagnosticUpdatable.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,22 @@
+package org.hps.users.kmccarty.diagpanel;
+
+import org.hps.users.kmccarty.DiagSnapshot;
+
+/**
+ * Interface <code>DiagnosticUpdatable</code> defines a class of objects
+ * that can be updated with information from a trigger diagnostic driver.
+ * They can take snapshots of the driver results and use this in order to
+ * alter their displayed or constituent values.
+ * 
+ * @author Kyle McCarty <[log in to unmask]>
+ * @see DiagSnapshot
+ */
+public interface DiagnosticUpdatable {
+	/**
+	 * Updates the object with information from the trigger diagnostic
+	 * snapshot in the argument.
+	 * @param snapshot - The snapshot containing information with which
+	 * to update the object.
+	 */
+	public void updatePanel(DiagSnapshot snapshot);
+}

Added: java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/TableTextModel.java
 =============================================================================
--- java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/TableTextModel.java	(added)
+++ java/trunk/users/src/main/java/org/hps/users/kmccarty/diagpanel/TableTextModel.java	Tue Mar  3 16:01:55 2015
@@ -0,0 +1,103 @@
+package org.hps.users.kmccarty.diagpanel;
+
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * Class <code>TableTextModel</code> is a simple implementation of
+ * <code>AbstractTableModel</code> that supports a definable number
+ * of rows and columns which must be populated with <code>String</code>
+ * data.
+ * 
+ * @author Kyle McCarty <[log in to unmask]>
+ */
+public class TableTextModel extends AbstractTableModel {
+	// Serial UID.
+	private static final long serialVersionUID = 0L;
+	
+	// Stored values.
+	private final int rows, columns;
+	private final String[][] values;
+	
+	/**
+	 * Instantiates a new <code>TableTextModel</code> with the indicated
+	 * number of rows and columns.
+	 * @param rows - The number of rows.
+	 * @param columns - The number of columns.
+	 */
+	public TableTextModel(int rows, int columns) {
+		// Make sure that the arguments for rows and columns are valid.
+		if(rows < 1) {
+			throw new IllegalArgumentException("TableTextModel must have at least one row.");
+		} else 	if(columns < 1) {
+			throw new IllegalArgumentException("TableTextModel must have at least one column.");
+		}
+		
+		// Define the number of rows and columns.
+		this.rows = rows;
+		this.columns = columns;
+		
+		// Instantiate the data storage array.
+		values = new String[rows][columns];
+	}
+	
+	@Override
+	public int getRowCount() { return rows; }
+	
+	@Override
+	public int getColumnCount() { return columns; }
+	
+	@Override
+	public Object getValueAt(int rowIndex, int columnIndex) {
+		// Ensure that the value is within the allowed range.
+		validateIndex(rowIndex, columnIndex);
+		
+		// Return the value.
+		return values[rowIndex][columnIndex];
+	}
+	
+	@Override
+	public void setValueAt(Object value, int rowIndex, int columnIndex) {
+		// If the object is a string, pass it to the preferred handler.
+		// This can also be performed if the value is null.
+		if(value == null || value instanceof String) {
+			setValueAt((String) value, rowIndex, columnIndex);
+		}
+		
+		// Otherwise, cast the object to a string and use that instead.
+		else { setValueAt(value.toString(), rowIndex, columnIndex); }
+	}
+	
+	/**
+	 * Sets the text for the indicated column and row of the table.
+	 * @param value - The new text.
+	 * @param rowIndex - The row.
+	 * @param columnIndex - The column.
+	 * @throws IndexOutOfBoundsException Occurs if the row and column
+	 * are not a valid member of table model.
+	 */
+	public void setValueAt(String value, int rowIndex, int columnIndex) throws IndexOutOfBoundsException {
+		// Ensure that the value is within the allowed range.
+		validateIndex(rowIndex, columnIndex);
+		
+		// Set the value.
+		values[rowIndex][columnIndex] = value;
+	}
+	
+	/**
+	 * Checks to make sure that a given row/column pointer refers to
+	 * an extant position in the data array. In the event that the row
+	 * and column values are not valid, an <code>IndexOutOfBounds</code>
+	 * exception is thrown.
+	 * @param rowIndex - The row index.
+	 * @param columnIndex - The column index.
+	 * @throws IndexOutOfBoundsException Occurs if the row and column
+	 * are not a valid member of the data array.
+	 */
+	private void validateIndex(int rowIndex, int columnIndex) throws IndexOutOfBoundsException {
+		if(rowIndex < 0 || rowIndex >= getRowCount()) {
+			throw new IndexOutOfBoundsException(String.format("Row index %d is out of bounds.", rowIndex));
+		} else if(columnIndex < 0 || columnIndex >= getColumnCount()) {
+			throw new IndexOutOfBoundsException(String.format("Column index %d is out of bounds.", columnIndex));
+		}
+	}
+}