Print

Print


Author: [log in to unmask]
Date: Thu Feb 12 13:00:39 2015
New Revision: 2123

Log:
PairTrigger now supports the ability to track whether a pair passed the time cut. TriggerDiagnosticDriver now contains a more robust cluster matching algorithm and additional framework toward an implementation of trigger matching.

Modified:
    java/trunk/users/src/main/java/org/hps/users/kmccarty/PairTrigger.java
    java/trunk/users/src/main/java/org/hps/users/kmccarty/TriggerDiagnosticDriver.java

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	Thu Feb 12 13:00:39 2015
@@ -9,6 +9,7 @@
 	private static final String PAIR_ENERGY_DIFFERENCE_HIGH = TriggerModule.PAIR_ENERGY_DIFFERENCE_HIGH;
 	private static final String PAIR_ENERGY_SLOPE_LOW = TriggerModule.PAIR_ENERGY_SLOPE_LOW;
 	private static final String PAIR_COPLANARITY_HIGH = TriggerModule.PAIR_COPLANARITY_HIGH;
+    private static final String PAIR_TIME_COINCIDENCE = "pairTimeCoincidence";
 	
 	/**
 	 * Instantiates a new <code>PairTrigger</code> with all cut
@@ -27,6 +28,7 @@
 		addValidCut(PAIR_ENERGY_DIFFERENCE_HIGH);
 		addValidCut(PAIR_ENERGY_SLOPE_LOW);
 		addValidCut(PAIR_COPLANARITY_HIGH);
+		addValidCut(PAIR_TIME_COINCIDENCE);
 	}
 	
 	/**
@@ -85,12 +87,21 @@
 	}
 	
 	/**
+	 * Gets whether the time coincidence cut was met.
+	 * @return Returns <code>true</code> if the cut was met and
+	 * <code>false</code> otherwise.
+	 */
+	public boolean getStateTimeCoincidence() {
+		return getCutState(PAIR_TIME_COINCIDENCE);
+	}
+	
+	/**
 	 * Sets whether the conditions for the pair energy sum lower bound
 	 * cut were met.
 	 * @param state - <code>true</code> indicates that the cut conditions
 	 * were met and <code>false</code> that they were not.
 	 */
-	public void getStateEnergySumLow(boolean state) {
+	public void setStateEnergySumLow(boolean state) {
 		setCutState(PAIR_ENERGY_SUM_LOW, state);
 	}
 	
@@ -100,7 +111,7 @@
 	 * @param state - <code>true</code> indicates that the cut conditions
 	 * were met and <code>false</code> that they were not.
 	 */
-	public void getStateEnergySumHigh(boolean state) {
+	public void setStateEnergySumHigh(boolean state) {
 		setCutState(PAIR_ENERGY_SUM_HIGH, state);
 	}
 	
@@ -110,7 +121,7 @@
 	 * @param state - <code>true</code> indicates that the cut conditions
 	 * were met and <code>false</code> that they were not.
 	 */
-	public void getStateEnergyDifference(boolean state) {
+	public void setStateEnergyDifference(boolean state) {
 		setCutState(PAIR_ENERGY_DIFFERENCE_HIGH, state);
 	}
 	
@@ -120,7 +131,7 @@
 	 * @param state - <code>true</code> indicates that the cut conditions
 	 * were met and <code>false</code> that they were not.
 	 */
-	public void getStateEnergySlope(boolean state) {
+	public void setStateEnergySlope(boolean state) {
 		setCutState(PAIR_ENERGY_SLOPE_LOW, state);
 	}
 	
@@ -130,7 +141,17 @@
 	 * @param state - <code>true</code> indicates that the cut conditions
 	 * were met and <code>false</code> that they were not.
 	 */
-	public void getStateCoplanarity(boolean state) {
+	public void setStateCoplanarity(boolean state) {
 		setCutState(PAIR_COPLANARITY_HIGH, state);
 	}
+	
+	/**
+	 * Sets whether the conditions for the time coincidence cut were
+	 * met.
+	 * @param state - <code>true</code> indicates that the cut conditions
+	 * were met and <code>false</code> that they were not.
+	 */
+	public void setStateTimeCoincidence(boolean state) {
+		setCutState(PAIR_TIME_COINCIDENCE, state);
+	}
 }

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	Thu Feb 12 13:00:39 2015
@@ -6,12 +6,16 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.Set;
 
 import org.hps.readout.ecal.TriggerModule;
 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.SSPSinglesTrigger;
+import org.hps.readout.ecal.triggerbank.SSPTrigger;
 import org.hps.readout.ecal.triggerbank.TIData;
 import org.hps.recon.ecal.CalorimeterHitUtilities;
 import org.lcsim.detector.converter.compact.EcalCrystal;
@@ -21,6 +25,7 @@
 import org.lcsim.event.GenericObject;
 import org.lcsim.geometry.Detector;
 import org.lcsim.util.Driver;
+import org.lcsim.util.log.LogUtil;
 
 public class TriggerDiagnosticDriver extends Driver {
 	// Store the LCIO collection names for the needed objects.
@@ -32,8 +37,8 @@
 	private SSPData sspBank;
 	private List<Cluster> reconClusters;
 	private List<SSPCluster> sspClusters;
-	private List<List<PairTrigger<Cluster>>> reconPairsTriggers = new ArrayList<List<PairTrigger<Cluster>>>(2);
-	private List<List<PairTrigger<SSPCluster>>> sspPairsTriggers = new ArrayList<List<PairTrigger<SSPCluster>>>(2);
+	private List<List<PairTrigger<Cluster[]>>> reconPairsTriggers = new ArrayList<List<PairTrigger<Cluster[]>>>(2);
+	private List<List<PairTrigger<SSPCluster[]>>> sspPairsTriggers = new ArrayList<List<PairTrigger<SSPCluster[]>>>(2);
 	private List<List<SinglesTrigger<Cluster>>> reconSinglesTriggers = new ArrayList<List<SinglesTrigger<Cluster>>>(2);
 	private List<List<SinglesTrigger<SSPCluster>>> sspSinglesTriggers = new ArrayList<List<SinglesTrigger<SSPCluster>>>(2);
 	
@@ -41,12 +46,26 @@
 	private TriggerModule[] singlesTrigger = new TriggerModule[2];
 	private TriggerModule[] pairsTrigger = new TriggerModule[2];
 	
-	// Store internal variables.
-	private double energyAcceptance = 0.05;
+	// Output text logger.
+	private static Level logLevel = Level.FINEST;
+	private static Logger logger = LogUtil.create(TriggerDiagnosticDriver.class);   
+	
+	// Verification settings.
+	private int nsa = 100;
+	private int nsb = 20;
+	private int windowWidth = 200;
+	private int hitAcceptance = 1;
+	private double energyAcceptance = 0.03;
+	private boolean performClusterVerification = true;
+	
+	// Efficiency tracking variables.
+	private int reconClustersFound = 0;
+	private int reconClustersMatched = 0;
 	
 	/*
 	@Override
 	public void detectorChanged(Detector detector) {
+		if(detector == null) { return; }
 		for(EcalCrystal crystal : detector.getSubdetector("Ecal").getDetectorElement().findDescendants(EcalCrystal.class)) {
 			System.out.println(crystal.getIdentifier().getValue());
 			CalorimeterHit tempHit = CalorimeterHitUtilities.create(1.000, 10.0, crystal.getIdentifier().getValue());
@@ -89,6 +108,7 @@
 		pairsTrigger[0].setCutValue(TriggerModule.PAIR_ENERGY_SLOPE_LOW, 0.000);
 		pairsTrigger[0].setCutValue(TriggerModule.PAIR_ENERGY_SLOPE_F, 0.001);
 		pairsTrigger[0].setCutValue(TriggerModule.PAIR_COPLANARITY_HIGH, 180);
+		pairsTrigger[0].setCutValue(TriggerModule.PAIR_TIME_COINCIDENCE, 8);
 		
 		// Define the second pairs trigger.
 		pairsTrigger[1] = new TriggerModule();
@@ -101,14 +121,32 @@
 		pairsTrigger[1].setCutValue(TriggerModule.PAIR_ENERGY_SLOPE_LOW, 0.000);
 		pairsTrigger[1].setCutValue(TriggerModule.PAIR_ENERGY_SLOPE_F, 0.001);
 		pairsTrigger[1].setCutValue(TriggerModule.PAIR_COPLANARITY_HIGH, 180);
+		pairsTrigger[1].setCutValue(TriggerModule.PAIR_TIME_COINCIDENCE, 8);
 		
 		// Instantiate the triggers lists.
 		for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
-			reconPairsTriggers.add(new ArrayList<PairTrigger<Cluster>>());
-			sspPairsTriggers.add(new ArrayList<PairTrigger<SSPCluster>>());
+			reconPairsTriggers.add(new ArrayList<PairTrigger<Cluster[]>>());
+			sspPairsTriggers.add(new ArrayList<PairTrigger<SSPCluster[]>>());
 			reconSinglesTriggers.add(new ArrayList<SinglesTrigger<Cluster>>());
 			sspSinglesTriggers.add(new ArrayList<SinglesTrigger<SSPCluster>>());
 		}
+		
+		// Set the logger level.
+		logger.setLevel(logLevel);
+		
+		// Print the initial settings.
+		logSettings();
+	}
+	
+	@Override
+	public void endOfData() {
+		System.out.println("\n==== Efficiency Report ===============================================");
+		System.out.println("======================================================================\n");
+		System.out.println("==== Cluster Verification ================");
+		System.out.printf("\tValid Clusters Reconstructed   :: %d%n", reconClustersFound);
+		System.out.printf("\tReconstructed Clusters Matched :: %d%n", reconClustersMatched);
+		System.out.printf("\tClustering Efficiency          :: %3.2f %%%n", (100.0 * reconClustersMatched / reconClustersFound));
+		
 	}
 	
 	/**
@@ -116,10 +154,24 @@
 	 */
 	@Override
 	public void process(EventHeader event) {
+		// ==========================================================
+		// ==== Obtain Reconstructed Clusters =======================
+		// ==========================================================
+		
 		// Get the reconstructed clusters.
 		if(event.hasCollection(Cluster.class, clusterCollectionName)) {
 			reconClusters = event.get(Cluster.class, clusterCollectionName);
-		}
+			logger.fine(String.format("%d reconstructed clusters found.", reconClusters.size()));
+		} else {
+			reconClusters = new ArrayList<Cluster>(0);
+			logger.warning(String.format("No reconstructed clusters were found for collection \"%s\" in this event.", clusterCollectionName));
+		}
+		
+		
+		
+		// ==========================================================
+		// ==== Obtain SSP and TI Banks =============================
+		// ==========================================================
 		
 		// Get the SSP clusters.
 		if(event.hasCollection(GenericObject.class, bankCollectionName)) {
@@ -131,83 +183,139 @@
 				// If this is an SSP bank, parse it.
 				if(AbstractIntData.getTag(obj) == SSPData.BANK_TAG) {
 					sspBank = new SSPData(obj);
+					logger.finer("Read SSP bank.");
 				}
 				
 				// Otherwise, if this is a TI bank, parse it.
 				else if(AbstractIntData.getTag(obj) == TIData.BANK_TAG) {
 					tiBank = new TIData(obj);
+					logger.finer("Read TI bank.");
 				}
 			}
 			
 			// If there is an SSP bank, get the list of SSP clusters.
 			if(sspBank != null) {
 				sspClusters = sspBank.getClusters();
-			}
-		}
-		
-		// Check that all of the collections and objects are present.
-		boolean allPresent = true;
+				logger.fine(String.format("%d SSP clusters found.", sspClusters.size()));
+			}
+		}
+		
+		
+		
+		// ==========================================================
+		// ==== Establish Event Integrity ===========================
+		// ==========================================================
+		
+		// Check that all of the required objects are present.
 		if(sspBank == null) {
-			System.out.println("SSP bank not found!");
-			allPresent = false;
+			logger.warning("No SSP bank found for this event. No verification will be performed.");
+			return;
 		} if(tiBank == null) {
-			System.out.println("TI bank not found!");
-			allPresent = false;
-		} if(sspClusters == null) {
-			System.out.println("SSP clusters not found!");
-			allPresent = false;
-		} if(reconClusters == null) {
-			System.out.println("Reconstructed clusters not found!");
-			allPresent = false;
-		}
-		
-		// Do nothing further if an object is missing.
-		if(!allPresent) { return; }
+			logger.warning("No TI bank found for this event. No verification will be performed.");
+			return;
+		}
+		
+		
+		
+		// ==========================================================
+		// ==== Perform Detailed Event Logging ======================
+		// ==========================================================
 		
 		// Otherwise, print out the two cluster collections.
-		System.out.printf("Summary for Event %d at time %d%n", event.getEventNumber(), event.getTimeStamp());
-		System.out.println("Reconstructed Clusters:");
+		logger.finest(String.format("Summary for Event %d at time %d", event.getEventNumber(), event.getTimeStamp()));
+		logger.finest("Reconstructed Clusters:");
 		for(Cluster cluster : reconClusters) {
-			System.out.println("\t" + reconClusterToString(cluster));
-		}
-		
-		System.out.println("SSP Clusters:");
+			logger.finest("\t" + reconClusterToString(cluster));
+		}
+		
+		logger.finest("SSP Clusters:");
 		for(SSPCluster cluster : sspClusters) {
-			System.out.println("\t" + sspClusterToString(cluster));
-		}
+			logger.finest("\t" + sspClusterToString(cluster));
+		}
+		
+		
+		
+		// ==========================================================
+		// ==== Perform Event Verification ==========================
+		// ==========================================================
 		
 		// Perform the cluster verification step.
-		verifyClusters();
+		if(performClusterVerification) { verifyClusters(); }
 		
 		// Construct lists of triggers for the SSP clusters and the
 		// reconstructed clusters.
-		constructTriggers();
-		
-		System.out.println("\n\n");
-	}
-	
+		constructSinglesTriggers();
+		constructPairTriggers();
+		verifyTriggers();
+	}
+	
+	/**
+	 * Attempts to match all reconstructed clusters that are safely
+	 * within the integration window with clusters reported by the SSP.
+	 * Method also tracks the ratio of valid reconstructed clusters to
+	 * matches found.<br/>
+	 * <br/>
+	 * Note that unmatched SSP clusters are ignored. Since these may
+	 * or may not correspond to reconstructed clusters that occur in
+	 * the forbidden time region, it is impossible to say whether or
+	 * not these legitimately failed to match or not.
+	 */
 	private void verifyClusters() {
+		// ==========================================================
+		// ==== Cluster Verification Initialization =================
+		// ==========================================================
+		
 		// Track which clusters match and whether a given cluster
 		// has been matched or not.
 		Set<Cluster> reconClusterSet = new HashSet<Cluster>(reconClusters.size());
 		Set<SSPCluster> sspClusterSet = new HashSet<SSPCluster>(sspClusters.size());
 		Map<Cluster, SSPCluster> pairMap = new HashMap<Cluster, SSPCluster>(reconClusters.size());
 		
+		// Store which clusters were rejected due to the cluster
+		// window size.
+		Set<Cluster> badClusters = new HashSet<Cluster>();
+		
+		
+		
+		// ==========================================================
+		// ==== Cluster Matching ====================================
+		// ==========================================================
+		
 		// Iterate over the reconstructed clusters and check whether
 		// there is a matching SSP cluster or not.
+		reconLoop:
 		for(Cluster reconCluster : reconClusters) {
 			// Get the cluster's x- and y- indices.
 			int ix = reconCluster.getCalorimeterHits().get(0).getIdentifierFieldValue("ix");
 			int iy = reconCluster.getCalorimeterHits().get(0).getIdentifierFieldValue("iy");
 			
+			// Require that the clusters are far enough way
+			// from the edges of the time window to ensure
+			// that they will not have the FADC pulse cut.
+			for(CalorimeterHit hit : reconCluster.getCalorimeterHits()) {
+				if(hit.getTime() <= nsb || hit.getTime() >= (windowWidth - nsa)) {
+					badClusters.add(reconCluster);
+					logger.finer(String.format("Cluster %s is out-of-time due to hit (%3d, %3d) at time %3.0f.",
+							reconClusterPositionString(reconCluster),
+							hit.getIdentifierFieldValue("ix"), hit.getIdentifierFieldValue("iy"),
+							hit.getTime()));
+					continue reconLoop;
+				}
+			}
+			
+			// Increment the number of reconstructed clusters found that
+			// are within the allowed time window.
+			reconClustersFound++;
+			
 			// Look for an unmatched cluster with the same indices.
 			matchLoop:
 			for(SSPCluster sspCluster : sspClusters) {
-				// TODO: Implement time cut on cluster matching.
 				if(sspCluster.getXIndex() == ix && sspCluster.getYIndex() == iy) {
 					// If this SSP cluster is already matched, it can
 					// not be used again.
 					if(sspClusterSet.contains(sspCluster)) {
+						logger.finer(String.format("Cluster %s fails to match SSP cluster %s; SSP cluster already matched.",
+								reconClusterPositionString(reconCluster), sspClusterPositionString(sspCluster)));
 						continue matchLoop;
 					}
 					
@@ -218,45 +326,131 @@
 							sspCluster.getEnergy() * (1 + energyAcceptance) };
 					
 					// If the energies are within range, consider this
-					// a matched cluster pair. They must also have the
-					// same hit count.
-					// TODO: Fix hit inconsistency bug.
-					if(reconCluster.getEnergy() >= energy[0] && reconCluster.getEnergy() <= energy[1]
-							&& reconCluster.getCalorimeterHits().size() == sspCluster.getHitCount()) {
-						// Add the two clusters to the matched sets.
-						sspClusterSet.add(sspCluster);
-						reconClusterSet.add(reconCluster);
-						
-						// Map the two clusters together.
-						pairMap.put(reconCluster, sspCluster);
-						
-						// Skip to the next recon cluster.
-						continue matchLoop;
+					// a matched cluster pair.
+					if(reconCluster.getEnergy() >= energy[0] && reconCluster.getEnergy() <= energy[1]) {
+						// Check that the clusters have the same hit
+						// count. If the allowHitCountVariance setting
+						// is enabled, this may differ by +/- 1 hit.
+						int reconHits = reconCluster.getCalorimeterHits().size();
+						int sspHits = sspCluster.getHitCount();
+						if((reconHits + hitAcceptance >= sspHits) && (reconHits - hitAcceptance <= sspHits)) {
+							// Add the two clusters to the matched sets.
+							sspClusterSet.add(sspCluster);
+							reconClusterSet.add(reconCluster);
+							
+							// Map the two clusters together.
+							pairMap.put(reconCluster, sspCluster);
+							
+							// Log the match.
+							logger.finer(String.format("Cluster %s matches SSP cluster %s",
+									reconClusterPositionString(reconCluster), sspClusterPositionString(sspCluster)));
+							
+							// Increment the number of reconstructed
+							// clusters matched.
+							reconClustersMatched++;
+							
+							// Skip to the next recon cluster.
+							continue matchLoop;
+						} else {
+							logger.finer(String.format("Cluster %s fails to match SSP cluster %s; hits not within threshold.",
+									reconClusterPositionString(reconCluster), sspClusterPositionString(sspCluster)));
+						} // End match hits check
+					} else {
+						logger.finer(String.format("Cluster %s fails to match SSP cluster %s; Energy not within %3.1f%%.",
+								reconClusterPositionString(reconCluster), sspClusterPositionString(sspCluster),
+								(energyAcceptance * 100.0)));
+					} // End match energy check
+				} // End match indices check
+			} // End matchLoop
+		} // End reconLoop
+		
+		
+		
+		// ==========================================================
+		// ==== Event Summary Readout ===============================
+		// ==========================================================
+		
+		// Output the matched clusters.
+		logger.finest("Matched Clusters:");
+		if(!pairMap.isEmpty()) {
+			for(Entry<Cluster, SSPCluster> pair : pairMap.entrySet()) {
+				logger.finest(String.format("\t%s --> %s%n", reconClusterToString(pair.getKey()), sspClusterToString(pair.getValue())));
+			}
+		} else { logger.finest("\tNone"); }
+		
+		// Output unmatched reconstructed clusters and SSP clusters.
+		logger.finest("\nUnmatched Clusters:");
+		if(sspClusterSet.size() != sspClusters.size() || reconClusterSet.size() != reconClusters.size()) {
+			// Output the SSP clusters that were not matched.
+			for(SSPCluster sspCluster : sspClusters) {
+				if(!sspClusterSet.contains(sspCluster)) {
+					logger.finest(String.format("\tSSP   :: %s", sspClusterToString(sspCluster)));
+				}
+			}
+			
+			// Output the recon clusters that were not matched.
+			for(Cluster reconCluster : reconClusters) {
+				if(!reconClusterSet.contains(reconCluster) && !badClusters.contains(reconCluster)) {
+					logger.finest(String.format("\tRecon :: %s", reconClusterToString(reconCluster)));
+				}
+			}
+		} else {  logger.finest("\tNone"); }
+		
+		// Output the reconstructed clusters that were out-of-time.
+		logger.finest("\nOut-of-time Recon Clusters:");
+		if(!badClusters.isEmpty()) {
+			for(Cluster badCluster : badClusters) {
+				logger.finest(String.format("\tRecon :: %s", reconClusterToString(badCluster)));
+			}
+		} else {  logger.finest("\tNone"); }
+		
+		// Output the event efficiency.
+		logger.fine(String.format("Event Efficiency: %.1f", (100.0 * pairMap.size() / (reconClusters.size() - badClusters.size()))));
+	}
+	
+	private void verifyTriggers() {
+		// Get the list of triggers reported by the SSP.
+		List<SSPTrigger> sspTriggers = sspBank.getTriggers();
+		
+		// Iterate over the triggers.
+		System.out.println("SSP Bank Singles Triggers:");
+		for(SSPTrigger sspTrigger : sspTriggers) {
+			// If the trigger is a singles trigger, convert it.
+			if(sspTrigger instanceof SSPSinglesTrigger) {
+				// Cast the trigger to a singles trigger.
+				SSPSinglesTrigger sspSingles = (SSPSinglesTrigger) sspTrigger;
+				int triggerNum = sspSingles.isFirstTrigger() ? 0 : 1;
+				boolean matchedTrigger = false;
+				
+				// Iterate over the SSP cluster simulated triggers and
+				// look for a cluster that matches.
+				matchLoop:
+				for(SinglesTrigger<SSPCluster> simTrigger : sspSinglesTriggers.get(triggerNum)) {
+					if(compareSSPSinglesTriggers(sspSingles, simTrigger)) {
+						matchedTrigger = true;
+						break matchLoop;
 					}
 				}
-			}
-		} // End matchLoop
-		
-		// Output the cluster matches and note which clusters failed
-		// to be paired. These may suggest an error.
-		System.out.println("Matched Clusters:");
-		for(Entry<Cluster, SSPCluster> pair : pairMap.entrySet()) {
-			System.out.printf("\t%s --> %s%n", reconClusterToString(pair.getKey()), sspClusterToString(pair.getValue()));
-		}
-		System.out.println("Unmatched Clusters:");
-		for(SSPCluster sspCluster : sspClusters){
-			if(!sspClusterSet.contains(sspCluster)) {
-				System.out.printf("\tSSP   :: %s%n", sspClusterToString(sspCluster));
-			}
-		}
-		for(Cluster reconCluster : reconClusters){
-			if(!reconClusterSet.contains(reconCluster)) {
-				System.out.printf("\tRecon :: %s%n", reconClusterToString(reconCluster));
-			}
-		}
-	}
-	
-	private void constructTriggers() {
+				
+				System.out.printf("\tTrigger %d :: %3d :: EClusterLow: %d; EClusterHigh %d; HitCount: %d :: Matched: %5b%n",
+						(triggerNum + 1), sspSingles.getTime(),
+						sspSingles.passCutEnergyMin() ? 1 : 0, sspSingles.passCutEnergyMax() ? 1 : 0,
+						sspSingles.passCutHitCount() ? 1 : 0, matchedTrigger);
+			}
+		}
+
+		System.out.println("SSP Clusters:");
+		for(SSPCluster cluster : sspClusters) {
+			System.out.println("\t" + sspClusterToString(cluster));
+		}
+	}
+	
+	/**
+	 * Generates and stores the singles triggers for both reconstructed
+	 * and SSP clusters.
+	 */
+	private void constructSinglesTriggers() {
+		System.out.println("SSP Cluster Singles Triggers:");
 		// Run the SSP clusters through the singles trigger to determine
 		// whether they pass it or not.
 		for(SSPCluster cluster : sspClusters) {
@@ -282,11 +476,16 @@
 				
 				// Store the trigger.
 				sspSinglesTriggers.get(triggerNum).add(trigger);
+				
+				System.out.printf("\tTrigger %d :: %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d%n",
+						(triggerNum + 1), sspClusterPositionString(cluster), passClusterLow ? 1 : 0,
+						passClusterHigh ? 1 : 0, passHitCount ? 1 : 0);
 			}
 		}
 		
 		// Run the reconstructed clusters through the singles trigger
 		// to determine whether they pass it or not.
+		System.out.println("Recon Cluster Singles Triggers:");
 		for(Cluster cluster : reconClusters) {
 			for(int triggerNum = 0; triggerNum < 2; triggerNum++) {
 				// For a cluster to have formed it is assumed to have passed
@@ -310,7 +509,210 @@
 				
 				// Store the trigger.
 				reconSinglesTriggers.get(triggerNum).add(trigger);
-			}
+				
+				System.out.printf("\tTrigger %d :: %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d%n",
+						(triggerNum + 1), reconClusterPositionString(cluster), passClusterLow ? 1 : 0,
+						passClusterHigh ? 1 : 0, passHitCount ? 1 : 0);
+			}
+		}
+	}
+	
+	/**
+	 * Generates and stores the pair triggers for both reconstructed
+	 * and SSP clusters.
+	 */
+	private void constructPairTriggers() {
+		// Store cluster pairs.
+		List<Cluster> topReconClusters = new ArrayList<Cluster>();
+		List<Cluster> bottomReconClusters = new ArrayList<Cluster>();
+		List<Cluster[]> reconPairs = new ArrayList<Cluster[]>();
+		List<SSPCluster> topSSPClusters = new ArrayList<SSPCluster>();
+		List<SSPCluster> bottomSSPClusters = new ArrayList<SSPCluster>();
+		List<SSPCluster[]> sspPairs = new ArrayList<SSPCluster[]>();
+		
+		// Split the clusters into lists of top and bottom clusters.
+		for(Cluster reconCluster : reconClusters) {
+			if(reconCluster.getCalorimeterHits().get(0).getIdentifierFieldValue("iy") > 0) {
+				topReconClusters.add(reconCluster);
+			} else {
+				bottomReconClusters.add(reconCluster);
+			}
+		}
+		for(SSPCluster sspCluster : sspClusters) {
+			if(sspCluster.getYIndex() > 0) {
+				topSSPClusters.add(sspCluster);
+			} else {
+				bottomSSPClusters.add(sspCluster);
+			}
+		}
+		
+		// Form all possible top/bottom cluster pairs.
+		for(Cluster topReconCluster : topReconClusters) {
+			for(Cluster bottomReconCluster : bottomReconClusters) {
+				Cluster[] reconPair = new Cluster[2];
+				reconPair[0] = topReconCluster;
+				reconPair[1] = bottomReconCluster;
+				reconPairs.add(reconPair);
+			}
+		}
+		for(SSPCluster topSSPCluster : topSSPClusters) {
+			for(SSPCluster bottomSSPCluster : bottomSSPClusters) {
+				SSPCluster[] sspPair = new SSPCluster[2];
+				sspPair[0] = topSSPCluster;
+				sspPair[1] = bottomSSPCluster;
+				sspPairs.add(sspPair);
+			}
+		}
+		
+		// Simulate the pair triggers and record the results.
+		System.out.println("Recon Cluster Pairs Triggers:");
+		for(Cluster[] reconPair : reconPairs) {
+			for(int triggerIndex = 0; triggerIndex < 2; triggerIndex++) {
+				// For a cluster to have formed it is assumed to have passed
+				// the cluster seed energy cuts. This can not be verified
+				// since the SSP bank does not report individual hit. 
+				boolean passSeedLow = true;
+				boolean passSeedHigh = true;
+				
+				// The remaining cuts may be acquired from trigger module.
+				boolean passClusterLow = pairsTrigger[triggerIndex].clusterTotalEnergyCutLow(reconPair[0])
+						&& pairsTrigger[triggerIndex].clusterTotalEnergyCutLow(reconPair[1]);
+				boolean passClusterHigh = pairsTrigger[triggerIndex].clusterTotalEnergyCutHigh(reconPair[0])
+						&& pairsTrigger[triggerIndex].clusterTotalEnergyCutHigh(reconPair[1]);
+				boolean passHitCount = pairsTrigger[triggerIndex].clusterHitCountCut(reconPair[0])
+						&& pairsTrigger[triggerIndex].clusterHitCountCut(reconPair[1]);
+				boolean passPairEnergySumLow = pairsTrigger[triggerIndex].pairEnergySumCutLow(reconPair);
+				boolean passPairEnergySumHigh = pairsTrigger[triggerIndex].pairEnergySumCutHigh(reconPair);
+				boolean passPairEnergyDifference = pairsTrigger[triggerIndex].pairEnergyDifferenceCut(reconPair);
+				boolean passPairEnergySlope = pairsTrigger[triggerIndex].pairEnergySlopeCut(reconPair);
+				boolean passPairCoplanarity = pairsTrigger[triggerIndex].pairCoplanarityCut(reconPair);
+				boolean passTimeCoincidence = pairsTrigger[triggerIndex].pairTimeCoincidenceCut(reconPair);
+				
+				// Create a trigger from the results.
+				PairTrigger<Cluster[]> trigger = new PairTrigger<Cluster[]>(reconPair);
+				trigger.setStateSeedEnergyLow(passSeedLow);
+				trigger.setStateSeedEnergyHigh(passSeedHigh);
+				trigger.setStateClusterEnergyLow(passClusterLow);
+				trigger.setStateClusterEnergyHigh(passClusterHigh);
+				trigger.setStateHitCount(passHitCount);
+				trigger.setStateEnergySumLow(passPairEnergySumLow);
+				trigger.setStateEnergySumHigh(passPairEnergySumHigh);
+				trigger.setStateEnergyDifference(passPairEnergyDifference);
+				trigger.setStateEnergySlope(passPairEnergySlope);
+				trigger.setStateCoplanarity(passPairCoplanarity);
+				trigger.setStateTimeCoincidence(passTimeCoincidence);
+				
+				// Add the trigger to the list.
+				reconPairsTriggers.get(triggerIndex).add(trigger);
+				
+				System.out.printf("\tTrigger %d :: %s, %s :: EClusterLow: %d; EClusterHigh %d; HitCount: %d; ESumLow: %d, ESumHigh: %d, EDiff: %d, ESlope: %d, Coplanarity: %d, TimeDiff: %d%n",
+						(triggerIndex + 1), reconClusterPositionString(reconPair[0]),
+						reconClusterPositionString(reconPair[1]), passClusterLow ? 1 : 0,
+						passClusterHigh ? 1 : 0, passHitCount ? 1 : 0, passPairEnergySumLow ? 1 : 0,
+						passPairEnergySumHigh ? 1 : 0, passPairEnergyDifference ? 1 : 0,
+						passPairEnergySlope ? 1 : 0, passPairCoplanarity ? 1 : 0,
+						passTimeCoincidence ? 1 : 0);
+			}
+		}
+		
+		for(SSPCluster[] sspPair : sspPairs) {
+			for(int triggerIndex = 0; triggerIndex < 2; triggerIndex++) {
+				// For a cluster to have formed it is assumed to have passed
+				// the cluster seed energy cuts. This can not be verified
+				// since the SSP bank does not report individual hit. 
+				boolean passSeedLow = true;
+				boolean passSeedHigh = true;
+				
+				// The remaining cuts may be acquired from trigger module.
+				boolean passClusterLow = pairsTrigger[triggerIndex].clusterTotalEnergyCutLow(sspPair[0])
+						&& pairsTrigger[triggerIndex].clusterTotalEnergyCutLow(sspPair[1]);
+				boolean passClusterHigh = pairsTrigger[triggerIndex].clusterTotalEnergyCutHigh(sspPair[0])
+						&& pairsTrigger[triggerIndex].clusterTotalEnergyCutHigh(sspPair[1]);
+				boolean passHitCount = pairsTrigger[triggerIndex].clusterHitCountCut(sspPair[0])
+						&& pairsTrigger[triggerIndex].clusterHitCountCut(sspPair[1]);
+				boolean passPairEnergySumLow = pairsTrigger[triggerIndex].pairEnergySumCutLow(sspPair);
+				boolean passPairEnergySumHigh = pairsTrigger[triggerIndex].pairEnergySumCutHigh(sspPair);
+				boolean passPairEnergyDifference = pairsTrigger[triggerIndex].pairEnergyDifferenceCut(sspPair);
+				
+				// TODO: Implement the pair energy slope cut and the and
+				//       pair coplanarity cut once they are supported by
+				//       TriggerModule.
+				boolean passPairEnergySlope = false;
+				boolean passPairCoplanarity = false;
+				
+				boolean passTimeCoincidence = pairsTrigger[triggerIndex].pairTimeCoincidenceCut(sspPair);
+				
+				// Create a trigger from the results.
+				PairTrigger<SSPCluster[]> trigger = new PairTrigger<SSPCluster[]>(sspPair);
+				trigger.setStateSeedEnergyLow(passSeedLow);
+				trigger.setStateSeedEnergyHigh(passSeedHigh);
+				trigger.setStateClusterEnergyLow(passClusterLow);
+				trigger.setStateClusterEnergyHigh(passClusterHigh);
+				trigger.setStateHitCount(passHitCount);
+				trigger.setStateEnergySumLow(passPairEnergySumLow);
+				trigger.setStateEnergySumHigh(passPairEnergySumHigh);
+				trigger.setStateEnergyDifference(passPairEnergyDifference);
+				trigger.setStateEnergySlope(passPairEnergySlope);
+				trigger.setStateCoplanarity(passPairCoplanarity);
+				trigger.setStateTimeCoincidence(passTimeCoincidence);
+				
+				// Add the trigger to the list.
+				//sspPairsTriggers.get(triggerIndex).add(trigger);
+			}
+		}
+	}
+	
+	/**
+	 * Outputs all of the verification parameters currently in use by
+	 * the software. A warning will be issued if the values for NSA and
+	 * NSB, along with the FADC window, preclude clusters from being
+	 * verified.
+	 */
+	private void logSettings() {
+		// Output general settings.
+		logger.config("Cluster Verification Settings");
+		logger.config(String.format("\tEnergy Threshold       :: %1.2f", energyAcceptance));
+		logger.config(String.format("\tHit Threshold          :: %1d", hitAcceptance));
+		
+		// Output window settings.
+		logger.config("FADC Timing Window Settings");
+		logger.config(String.format("\tNSB                    :: %3d ns", nsb));
+		logger.config(String.format("\tNSA                    :: %3d ns", nsa));
+		logger.config(String.format("\tFADC Window            :: %3d ns", windowWidth));
+		
+		// Calculate the valid clustering window.
+		int start = nsb;
+		int end = windowWidth - nsa;
+		if(start < end) {
+			logger.config(String.format("\tValid Cluster Window   :: [ %3d ns, %3d ns ]", start, end));
+			performClusterVerification = true;
+		} else {
+			logger.warning("\tNSB, NSA, and FADC window preclude a valid cluster verification window.");
+			logger.warning("\tCluster verification will not be performed!");
+			performClusterVerification = false;
+		}
+		
+		// Output the singles trigger settings.
+		for(int i = 0; i < 2; i++) {
+			logger.config(String.format("Singles Trigger %d Settings", (i + 1)));
+			logger.config(String.format("\tCluster Energy Low     :: %.3f GeV", singlesTrigger[i].getCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_LOW)));
+			logger.config(String.format("\tCluster Energy High    :: %.3f GeV", singlesTrigger[i].getCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_HIGH)));
+			logger.config(String.format("\tCluster Hit Count      :: %.0f hits", singlesTrigger[i].getCutValue(TriggerModule.CLUSTER_HIT_COUNT_LOW)));
+		}
+		
+		// Output the pair trigger settings.
+		for(int i = 0; i < 2; i++) {
+			logger.config(String.format("Pairs Trigger %d Settings", (i + 1)));
+			logger.config(String.format("\tCluster Energy Low     :: %.3f GeV", pairsTrigger[i].getCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_LOW)));
+			logger.config(String.format("\tCluster Energy High    :: %.3f Gen", pairsTrigger[i].getCutValue(TriggerModule.CLUSTER_TOTAL_ENERGY_HIGH)));
+			logger.config(String.format("\tCluster Hit Count      :: %.0f hits", pairsTrigger[i].getCutValue(TriggerModule.CLUSTER_HIT_COUNT_LOW)));
+			logger.config(String.format("\tPair Energy Sum Low    :: %.3f GeV", pairsTrigger[i].getCutValue(TriggerModule.PAIR_ENERGY_SUM_LOW)));
+			logger.config(String.format("\tPair Energy Sum Low    :: %.3f GeV", pairsTrigger[i].getCutValue(TriggerModule.PAIR_ENERGY_SUM_HIGH)));
+			logger.config(String.format("\tPair Energy Difference :: %.3f GeV", pairsTrigger[i].getCutValue(TriggerModule.PAIR_ENERGY_DIFFERENCE_HIGH)));
+			logger.config(String.format("\tPair Energy Slope      :: %.3f GeV", pairsTrigger[i].getCutValue(TriggerModule.PAIR_ENERGY_SLOPE_LOW)));
+			logger.config(String.format("\tPair Energy Slope F    :: %.3f GeV / mm", pairsTrigger[i].getCutValue(TriggerModule.PAIR_ENERGY_SLOPE_F)));
+			logger.config(String.format("\tPair Coplanarity       :: %.0f Degrees", pairsTrigger[i].getCutValue(TriggerModule.PAIR_COPLANARITY_HIGH)));
+			logger.config(String.format("\tPair Time Coincidence  :: %.0f ns", pairsTrigger[i].getCutValue(TriggerModule.PAIR_TIME_COINCIDENCE)));
 		}
 	}
 	
@@ -327,4 +729,49 @@
 				cluster.getXIndex(), cluster.getYIndex(), cluster.getEnergy(),
 				cluster.getHitCount(), cluster.getTime());
 	}
+	
+	private static final String reconClusterPositionString(Cluster cluster) {
+		return String.format("(%3d, %3d)",
+				cluster.getCalorimeterHits().get(0).getIdentifierFieldValue("ix"),
+				cluster.getCalorimeterHits().get(0).getIdentifierFieldValue("iy"));
+	}
+	
+	private static final String sspClusterPositionString(SSPCluster cluster) {
+		return String.format("(%3d, %3d)", cluster.getXIndex(), cluster.getYIndex());
+	}
+	
+	/**
+	 * Compares a trigger from the SSP bank to a trigger simulated on
+	 * an SSP 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 compareSSPSinglesTriggers(SSPSinglesTrigger bankTrigger, SinglesTrigger<SSPCluster> simTrigger) {
+		// The bank trigger and simulated trigger must have the same
+		// time. This is equivalent to the time of the triggering cluster.
+		if(bankTrigger.getTime() != simTrigger.getTriggerSource().getTime()) {
+			return false;
+		}
+		
+		// If the time stamp is the same, check that the trigger flags
+		// are all the same. Start with cluster energy low.
+		if(bankTrigger.passCutEnergyMin() != simTrigger.getStateClusterEnergyLow()) {
+			return false;
+		}
+		
+		// Check cluster energy high.
+		if(bankTrigger.passCutEnergyMax() != simTrigger.getStateClusterEnergyHigh()) {
+			return false;
+		}
+		
+		// Check cluster hit count.
+		if(bankTrigger.passCutHitCount() != simTrigger.getStateHitCount()) {
+			return false;
+		}
+		
+		// If all of the tests are successful, the triggers match.
+		return true;
+	}
 }