Print

Print


Author: [log in to unmask]
Date: Fri Jan  9 16:24:39 2015
New Revision: 1912

Log:
Add several more clustering algorithm implementations to the ecal.cluster package.

Added:
    java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/CTPClusterer.java
    java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterDriver.java
    java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterer.java
Modified:
    java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/CTPEcalClusterer.java
    java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/GTPEcalClusterer.java
    java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/ClustererFactory.java
    java/trunk/ecal-recon/src/test/java/org/hps/recon/ecal/cluster/ClustererTest.java

Modified: java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/CTPEcalClusterer.java
 =============================================================================
--- java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/CTPEcalClusterer.java	(original)
+++ java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/CTPEcalClusterer.java	Fri Jan  9 16:24:39 2015
@@ -28,8 +28,10 @@
  * @author Jeremy McCormick <[log in to unmask]>
  * @author Tim Nelson <[log in to unmask]>
  * @author Sho Uemura <[log in to unmask]>
- * @version $Id: CTPEcalClusterer.java,v 1.1 2013/02/25 22:39:24 meeg Exp $
+ * 
+ * @deprecated Use the {@link org.hps.recon.ecal.cluster.CTPClusterer} with the {@link org.hps.recon.ecal.cluster.ClusterDriver}.
  */
+@Deprecated
 public class CTPEcalClusterer extends Driver {
 	
 	Detector detector = null;

Modified: java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/GTPEcalClusterer.java
 =============================================================================
--- java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/GTPEcalClusterer.java	(original)
+++ java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/GTPEcalClusterer.java	Fri Jan  9 16:24:39 2015
@@ -30,7 +30,10 @@
  * either at the same location as the seed hit or is a neighbor to the seed hit.
  * @author Kyle McCarty
  * @author Sho Uemura
+ * 
+ * @deprecated Use the {@link org.hps.recon.ecal.cluster.GTPClusterDriver} instead.
  */
+@Deprecated
 public class GTPEcalClusterer extends Driver {
 	Detector detector = null;
 	

Added: java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/CTPClusterer.java
 =============================================================================
--- java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/CTPClusterer.java	(added)
+++ java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/CTPClusterer.java	Fri Jan  9 16:24:39 2015
@@ -0,0 +1,332 @@
+package org.hps.recon.ecal.cluster;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+
+import org.hps.recon.ecal.CalorimeterHitUtilities;
+import org.lcsim.conditions.ConditionsEvent;
+import org.lcsim.event.CalorimeterHit;
+import org.lcsim.event.Cluster;
+import org.lcsim.event.EventHeader;
+import org.lcsim.event.base.BaseCalorimeterHit;
+import org.lcsim.event.base.BaseCluster;
+import org.lcsim.geometry.IDDecoder;
+
+/**
+ * Creates clusters from CalorimeterHits in the HPSEcal detector.
+ *
+ * The clustering algorithm is from JLab Hall B 6 GeV DVCS Trigger Design doc.
+ *
+ * @author Kyle McCarty
+ * @author Sho Uemura <[log in to unmask]>
+ * @author Jeremy McCormick <[log in to unmask]>
+ */
+public class CTPClusterer extends AbstractClusterer {
+
+    IDDecoder dec;
+    
+    Set<Long> clusterCenters = null;
+    Map<Long, Double> hitSums = null;
+    Map<Long, CalorimeterHit> hitMap = null;
+
+    // The time period in which clusters may be formed. A negative value means that all hits
+    // will always be used in cluster finding, regardless of the time difference between them.
+    double clusterWindow = -1;
+    
+    // The minimum energy needed for a hit to be considered.
+    double addEMin = 0;
+    
+    CTPClusterer() {
+        super(new String[] { "addEMin", "clusterWindow"}, new double[] { 0., -1. });
+    }
+    
+    public void initialize() {
+        addEMin = getCuts().getValue("addEMin");
+        clusterWindow = getCuts().getValue("clusterWindow");
+    }
+         
+    @Override
+    public void conditionsChanged(ConditionsEvent event) {
+        super.conditionsChanged(event);
+        
+        // Get the decoder for the ECal IDs.
+        dec = ecal.getIDDecoder();    
+                
+        // Make set of valid cluster centers.
+        // Exclude edge crystals as good cluster centers.
+        clusterCenters = new HashSet<Long>();
+        for (Long cellID : neighborMap.keySet()) {
+            boolean isValidCenter = true;
+            Set<Long> neighbors = neighborMap.get(cellID);
+            for (Long neighborID : neighbors) {
+                Set<Long> neighborneighbors = new HashSet<Long>();
+                neighborneighbors.addAll(neighborMap.get(neighborID));
+                neighborneighbors.add(neighborID);
+
+                if (neighborneighbors.containsAll(neighbors)) {
+                    isValidCenter = false;
+                    break;
+                }
+            }
+            if (isValidCenter) {
+                clusterCenters.add(cellID);
+            }
+        }
+    }
+
+    @Override
+    public List<Cluster> createClusters(EventHeader event, List<CalorimeterHit> hits) {
+
+        // Define a list of clusters to be filled.
+        List<Cluster> clusters;
+
+        // If there is a cluster window, run the cluster window code. A cluster window is a time
+        // period in nanoseconds during which hits can be applied to the same cluster.
+        if (clusterWindow >= 0) {
+            // Create priority queues. These are sorted by the time variable associated with each hit.
+            PriorityQueue<CalorimeterHit> futureHits = new PriorityQueue<CalorimeterHit>(10, new TimeComparator());
+            PriorityQueue<CalorimeterHit> pastHits = new PriorityQueue<CalorimeterHit>(10, new TimeComparator());
+
+            // Initialize the cluster list variable.
+            clusters = new ArrayList<Cluster>();
+
+            // Populate the list of unprocessed hits with the calorimeter hits. These will then be sorted
+            // by time, from first to last, automatically by the priority queue.
+            for (CalorimeterHit hit : hits) {
+                if (hit.getRawEnergy() > addEMin) {
+                    futureHits.add(hit);
+                }
+            }
+
+            // We process the unprocessed hits...
+            while (!futureHits.isEmpty()) {
+                // Move the first occurring hit from the unprocessed list to the processed list.
+                CalorimeterHit nextHit = futureHits.poll();
+                pastHits.add(nextHit);
+
+                // Add any hits that occurred at the same time as the hit we just added to the processed list.
+                while (!futureHits.isEmpty() && futureHits.peek().getTime() == nextHit.getTime()) {
+                    pastHits.add(futureHits.poll());
+                }
+
+                // Remove hits that happened earlier than the cluster window period.
+                while (pastHits.peek().getTime() < nextHit.getTime() - clusterWindow) {
+                    pastHits.poll();
+                }
+
+                // Calculate the cluster energy for each crystal. This should be the
+                // total energy for the 3x3 crystal collection sorrounding the center
+                // crystal.
+                sumHits(pastHits);
+
+                // Choose which crystal is the appropriate cluster crystal.
+                clusters.addAll(createClusters());
+            }
+            // If there is no cluster window, then all the hits in the event are visible simultaneously.
+        } else {
+            // Calculate the cluster energy of each crystal.
+            sumHits(hits);
+            // Generate the clusters.
+            clusters = createClusters();
+        }
+        
+        return clusters;
+    }
+
+    private void sumHits(Collection<CalorimeterHit> hits) {
+        // Store the latest hit on each crystal in a map for later reference in
+        // the clustering algorithm.
+        hitMap = new HashMap<Long, CalorimeterHit>();
+        // Store the cluster energy for each crystal. Cluster energy represents
+        // the total energy of the 3x3 crystal set.
+        hitSums = new HashMap<Long, Double>();
+
+        // Loop over the active calorimeter hits to compute the cluster energies.
+        for (CalorimeterHit hit : hits) {
+            // Make a hit map for quick lookup by ID.
+            hitMap.put(hit.getCellID(), hit);
+
+            // Get the cell ID for the current crystal's neighbors.
+            Set<Long> neighbors = neighborMap.get(hit.getCellID());
+
+            // If there are no neighbors, something is rather wrong.
+            if (neighbors == null) {
+                throw new RuntimeException("Oops!  Set of neighbors is null!");
+            }
+
+            // Store the energy of the current calorimeter hit.
+            Double hitSum;
+
+            // We are only interested in this crystal's cluster energy if it is
+            // a valid cluster crystal. Edge crystals are not allowed to be clusters,
+            // so these are ignored.
+            if (clusterCenters.contains(hit.getCellID())) {
+                // Check if an energy has been assigned to the crystal.
+                hitSum = hitSums.get(hit.getCellID());
+
+                // If not, then the crystal's cluster energy is equal to this hit's energy.
+                if (hitSum == null) {
+                    hitSums.put(hit.getCellID(), hit.getRawEnergy());
+                }
+                // Otherwise, add the energy of this hit to the total crystal cluster energy.
+                else {
+                    hitSums.put(hit.getCellID(), hitSum + hit.getRawEnergy());
+                }
+            }
+
+            // Loop over neighbors to add the current hit's energy to the neighbor's
+            // cluster energy.
+            for (Long neighborId : neighbors) {
+                // If the crystal is not an edge crystal, ignore its hit energy.
+                if (!clusterCenters.contains(neighborId)) {
+                    continue;
+                }
+
+                // Get the cluster energy of the neighboring crystals.
+                hitSum = hitSums.get(neighborId);
+
+                // If the neighbor crystal has no cluster energy, then set the
+                // cluster energy to the current hit's energy.
+                if (hitSum == null) {
+                    hitSums.put(neighborId, hit.getRawEnergy());
+                }
+                // Otherwise, add the hit's energy to the neighbor's cluster energy.
+                else {
+                    hitSums.put(neighborId, hitSum + hit.getRawEnergy());
+                }
+            }
+        }
+    }
+
+    private List<Cluster> createClusters() {
+        // Create a list of clusters to be added to the event,
+        List<Cluster> clusters = new ArrayList<Cluster>();
+
+        // We examine each crystal with a non-zero cluster energy.
+        for(Long possibleCluster : hitSums.keySet()) {
+            // Get the luster energy for the crystal this hit is assocaite with.
+            Double thisSum = hitSums.get(possibleCluster);
+
+            // Get neighboring crystals' IDs.
+            Set<Long> neighbors = neighborMap.get(possibleCluster);
+
+            // If there are no neighbors, throw an error.
+            if (neighbors == null) {
+                throw new RuntimeException("Oops!  Set of neighbors is null!");
+            }
+
+            // Get the x/y position of the hit's associated crystal.
+            dec.setID(possibleCluster);
+            int x1 = dec.getValue("ix");
+            int y1 = dec.getValue("iy");
+
+            // Store whether it is a valid cluster or not.
+            boolean isCluster = true;
+
+            // Check to see if any of the crystal's neighbors preclude the current crystal
+            // from being a proper cluster. The cluster crystal should have the highest
+            // energy among its neighbors
+            for (Long neighborId : neighbors) {
+                // Get the x/y position of the neighbor's associated crystal.
+                dec.setID(neighborId);
+                int x2 = dec.getValue("ix");
+                int y2 = dec.getValue("iy");
+
+                // If the neighbor's energy value does not exist, we don't need to perform
+                // any additional checks for this neighbor. A crystal with no energy can
+                // not be the center of a cluster.
+                Double neighborSum = hitSums.get(neighborId);
+                if (neighborSum == null) {
+                    continue;
+                }
+
+                // If the neighbor's energy value is greater than this crystal's value,
+                // then this crystal is not the cluster and we may terminate the check.
+                if (neighborSum > thisSum) {
+                    isCluster = false;
+                    break;
+                }
+                // If the crystals each have the same energy, we choose the crystal that
+                // is closest to the electron side of the detector. If both crystals are
+                // equally close, we choose the crystal closest to the beam gap. If the
+                // neighbor fits these parameters better, this is not a crystal and we
+                // may skip any further checks.
+                else if (neighborSum.equals(thisSum) && (x1 > x2 || (x1 == x2 && Math.abs(y1) < Math.abs(y2)))) {
+                    isCluster = false;
+                    break;
+                }
+            }
+
+            // If the crystal was not invalidated by the any of the neighboring crystals,
+            // then it is a cluster crystal and should be processed.
+            if (isCluster) {
+                // Make a list to store the hits that are part of this cluster.
+                List<CalorimeterHit> hits = new ArrayList<CalorimeterHit>();
+
+                // Store the time at which the cluster occurred.
+                double clusterTime = Double.NEGATIVE_INFINITY;
+
+                // Get the last hit on this crystal.
+                CalorimeterHit hit = hitMap.get(possibleCluster);
+
+                // If the hit exists, add it to the list of associated hits.
+                if (hit != null) {
+                    hits.add(hit);
+
+                    // If the latest hit's time is later than the current cluster time,
+                    // set the cluster time to the latest hit's time.
+                    if (hit.getTime() > clusterTime) {
+                        clusterTime = hit.getTime();
+                    }
+                }
+
+                // Add all of the neighboring crystals to the cluster, if they have a
+                // hit associated with them. Crystals with no hits are not actually part
+                // of a cluster.
+                for (Long neighborId : neighbors) {
+                    hit = hitMap.get(neighborId);
+                    if (hit != null) {
+                        hits.add(hit);
+                        if (hit.getTime() > clusterTime) {
+                            clusterTime = hit.getTime();
+                        }
+                    }
+                }
+
+                // Generate a new cluster seed hit from the above results.
+                CalorimeterHit seedHit = (BaseCalorimeterHit)CalorimeterHitUtilities.create(0.0, clusterTime, possibleCluster, hits.get(0).getMetaData());
+
+                // Generate a new cluster from the seed hit.
+                BaseCluster cluster = new BaseCluster();
+                cluster.addHit(seedHit);
+                // Populate the cluster with each of the chosen neighbors.
+                for (CalorimeterHit clusterHit : hits) {
+                    cluster.addHit(clusterHit);
+                }
+                // Add the cluster to the cluster list.
+                clusters.add(cluster);
+            }
+        }
+
+        // Return the list of clusters.
+        return clusters;
+    }
+
+    static class TimeComparator implements Comparator<CalorimeterHit> {
+        // Compare by time with the earlier coming before the later.
+        public int compare(CalorimeterHit o1, CalorimeterHit o2) {
+            if (o1.getTime() == o2.getTime()) {
+                return 0;
+            } else {
+                return (o1.getTime() > o2.getTime()) ? 1 : -1;
+            }
+        }
+    }
+}

Modified: java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/ClustererFactory.java
 =============================================================================
--- java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/ClustererFactory.java	(original)
+++ java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/ClustererFactory.java	Fri Jan  9 16:24:39 2015
@@ -13,13 +13,15 @@
  * this class implements the {@link Clusterer} interface and will throw an error if it does not.
  * 
  * @see Clusterer
+ * @see CTPClusterer
  * @see DualThresholdCosmicClusterer
+ * @see GTPClusterer
  * @see GTPOnlineClusterer
  * @see LegacyClusterer 
  * @see NearestNeighborClusterer
  * @see ReconClusterer
  * @see SimpleReconClusterer
- * @see SimpleCosmicClusterer
+ * @see SimpleCosmicClusterer 
  * 
  * @author Jeremy McCormick <[log in to unmask]>
  */
@@ -54,6 +56,10 @@
             clusterer = new SimpleCosmicClusterer();
         } else if (GTPOnlineClusterer.class.getSimpleName().equals(name)) {
             clusterer = new GTPOnlineClusterer();
+        } else if (GTPClusterer.class.getSimpleName().equals(name)) {
+            clusterer = new GTPClusterer();
+        } else if (CTPClusterer.class.getSimpleName().equals(name)) {
+            clusterer = new CTPClusterer();
         } else {
             // Try to instantiate a Clusterer object from the name argument, assuming it is a canonical class name.
             try {

Added: java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterDriver.java
 =============================================================================
--- java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterDriver.java	(added)
+++ java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterDriver.java	Fri Jan  9 16:24:39 2015
@@ -0,0 +1,31 @@
+package org.hps.recon.ecal.cluster;
+
+/**
+ * This is a Driver to wrap the GTPClusterer algorithm,
+ * allowing the <code>limitClusterRange</code> to be
+ * set publicly.
+ * 
+ * @see GTPClusterer
+ * 
+ * @author Jeremy McCormick <[log in to unmask]>
+ */
+public class GTPClusterDriver extends ClusterDriver {
+    
+    public GTPClusterDriver() {
+        clusterer = ClustererFactory.create("GTPClusterer");
+    }
+    
+    /**
+     * Sets whether hits should be added to a cluster from the entire
+     * cluster window or just the "future" hits, plus one clock-cycle
+     * of "past" hits as a safety buffer to account for time uncertainty.
+     * 
+     * @param limitClusterRange - <code>true</code> indicates that
+     * the asymmetric clustering window should be used and <code>
+     * false</code> that the symmetric window should be used.
+     */
+    void setLimitClusterRange(boolean limitClusterRange) {
+        GTPClusterer gtpClusterer = getClusterer();
+        gtpClusterer.setLimitClusterRange(limitClusterRange);
+    }        
+}

Added: java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterer.java
 =============================================================================
--- java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterer.java	(added)
+++ java/trunk/ecal-recon/src/main/java/org/hps/recon/ecal/cluster/GTPClusterer.java	Fri Jan  9 16:24:39 2015
@@ -0,0 +1,271 @@
+package org.hps.recon.ecal.cluster;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.lcsim.event.CalorimeterHit;
+import org.lcsim.event.Cluster;
+import org.lcsim.event.EventHeader;
+import org.lcsim.event.base.BaseCluster;
+
+/**
+ * Class <code>GTPCalorimeterClusterer</code> processes events and converts hits
+ * into clusters, where appropriate. It uses the modified 2014 clustering algorithm.<br/>
+ * <br/>
+ * For a hit to be a cluster center, it is required to have an energy above some
+ * tunable minimum threshold. Additionally, the hit must be a local maximum with
+ * respect to its neighbors and itself over a tunable (default 2) clock cycles.
+ * Hits that pass these checks are then required to additional have a total
+ * cluster energy that exceeds another tunable minimum threshold.<br/>
+ * <br/>
+ * A hit is added to a cluster as a component if it has a non-zero energy and
+ * within the aforementioned tunable time buffer used for clustering and is
+ * either at the same location as the seed hit or is a neighbor to the seed hit.
+ * @author Kyle McCarty
+ * @author Sho Uemura
+ */
+public class GTPClusterer extends AbstractClusterer {
+        
+    /**
+     * <b>seedEnergyThreshold</b><br/><br/>
+     * <code>private double <b>seedEnergyThreshold</b></code><br/><br/>
+     * The minimum energy required for a hit to be considered as a cluster
+     * center. Hits with energy less than this value will be ignored.
+     */
+    private double seedEnergyThreshold;
+    
+    /**
+     * <b>clusterWindow</b><br/><br/>
+     * <code>private int <b>clusterWindow</b></code><br/><br/>
+     * Indicates the number of FADC clock cycles (each cycle is 4 ns) before and
+     * after a given cycle that should be considered when checking if a cluster
+     * is a local maximum in space-time.
+     */
+    private int clusterWindow;
+    
+    /**
+     * <b>hitBuffer</b><br/><br/>
+     * <code>private LinkedList<List<CalorimeterHit>> <b>hitBuffer</b></code><br/><br/>
+     * Stores a set of all the hits occurring in each clock cycle for the number
+     * of clock cycles that should be considered for clustering.
+     */
+    private LinkedList<Map<Long, CalorimeterHit>> hitBuffer;
+
+    /**
+     * <b>limitClusterRange</b><br/><br/>
+     * <code>private boolean <b>limitClusterRange</b></code><br/><br/>
+     * Whether an asymmetric or symmetric window should be used for
+     * adding hits to a cluster.
+     */
+    private boolean limitClusterRange = false;
+    
+    GTPClusterer() {
+        super(new String[] { "seedEnergyThreshold", "clusterWindow" }, new double[] { 0.05, 2.});
+    }    
+    
+    public void initialize() {
+        
+        // Set cuts.
+        setSeedEnergyThreshold(getCuts().getValue("seedEnergyThreshold"));
+        setClusterWindow((int) getCuts().getValue("clusterWindow"));
+        
+        // Initiate the hit buffer.
+        hitBuffer = new LinkedList<Map<Long, CalorimeterHit>>();
+        
+        // Populate the event buffer with (2 * clusterWindow + 1)
+        // empty events. These empty events represent the fact that
+        // the first few events will not have any events in the past
+        // portion of the buffer.
+        int bufferSize = (2 * clusterWindow) + 1;
+        for (int i = 0; i < bufferSize; i++) {
+            hitBuffer.add(new HashMap<Long, CalorimeterHit>(0));
+        }
+    }
+        
+    /**
+     * Generates a list of clusters from the current hit buffer. The "present"
+     * event is taken to be the list of hits occurring at index
+     * <code>clusterWindow</code>, which is the middle of the buffer.
+     *
+     * @return Returns a <code>List</code> of <code>HPSEcalCluster
+     * </code> objects generated from the current event.
+     */
+    private List<Cluster> getClusters() {
+        // Generate a list for storing clusters.
+        List<Cluster> clusters = new ArrayList<Cluster>();
+        
+        // Get the list of hits at the current time in the event buffer.
+        Map<Long, CalorimeterHit> currentHits = hitBuffer.get(clusterWindow);
+        
+        // For a hit to be a cluster center, it must be a local maximum
+        // both with respect to its neighbors and itself both in the
+        // present time and at all times within the event buffer.
+        seedLoop:
+        for (Long currentID : currentHits.keySet()) {
+            // Get the actual hit object.
+            CalorimeterHit currentHit = currentHits.get(currentID);
+            
+            // Store the energy of the current hit.
+            double currentEnergy = currentHit.getRawEnergy();
+            
+            // If the hit energy is lower than the minimum threshold,
+            // then we immediately reject this hit as a possible cluster.
+            if (currentEnergy < seedEnergyThreshold) {
+                continue seedLoop;
+            }
+            
+            // Store the crystals that are part of this potential cluster, 
+            // starting with the cluster seed candidate.
+            BaseCluster cluster = new BaseCluster();            
+            cluster.addHit(currentHit);
+            
+            // Get the set of neighbors for this hit.
+            Set<Long> neighbors = neighborMap.get(currentHit.getCellID());
+            
+            // Sort through each event stored in the buffer.
+            int bufferIndex = 0;
+            for (Map<Long, CalorimeterHit> bufferHits : hitBuffer) {
+                // Get the hit energy at the current hit's position in
+                // the buffer, if it exists. Ignore the current seed candidate.
+                CalorimeterHit bufferHit = bufferHits.get(currentID);
+                if (bufferHit != null && bufferHit != currentHit) {
+                    double bufferHitEnergy = bufferHit.getRawEnergy();
+                    
+                    // Check to see if the hit at this point in the buffer
+                    // is larger than then original hit. If it is, we may
+                    // stop the comparison because this is not a cluster.
+                    if (bufferHitEnergy > currentEnergy) {
+                        continue seedLoop;
+                    }
+                    
+                    // If the buffer hit is smaller, then add its energy
+                    // to the cluster total energy.
+                    else {
+                        if(limitClusterRange && bufferIndex <= clusterWindow + 1) { cluster.addHit(bufferHit); }
+                        else if(!limitClusterRange) { cluster.addHit(bufferHit); }
+                    }
+                }
+                
+                // We must also make sure that the original hit is
+                // larger than all of the neighboring hits at this
+                // point in the buffer as well.
+                for (Long neighborID : neighbors) {
+                    // Get the neighbor hit energy if it exists.
+                    CalorimeterHit neighborHit = bufferHits.get(neighborID);
+                    if (neighborHit != null) {
+                        double neighborHitEnergy = neighborHit.getRawEnergy();
+                        
+                        // Check to see if the neighbor hit at this point
+                        // in the buffer is larger than then original hit.
+                        // If it is, we may stop the comparison because this
+                        // is not a cluster.
+                        if (neighborHitEnergy > currentEnergy) {
+                            continue seedLoop;
+                        }
+                        
+                        // If the buffer neighbor hit is smaller, then
+                        // add its energy to the cluster total energy.
+                        else {
+                            if(limitClusterRange && bufferIndex <= clusterWindow + 1) { cluster.addHit(neighborHit); }
+                            else if(!limitClusterRange) { cluster.addHit(neighborHit); }
+                        }
+                    }
+                }
+                
+                // Increment the buffer index.
+                bufferIndex++;
+            }
+            
+            // Add the cluster to the list of clusters.
+            clusters.add(cluster);
+        }
+        
+        // Return the generated list of clusters.
+        return clusters;
+    }
+    
+    /**
+     * Places hits from the current event into the event hit buffer and
+     * processes the buffer to extract clusters. Clusters are then stored in the
+     * event object.
+     *
+     * @param event - The event to process.
+     */
+    public List<Cluster> createClusters(EventHeader event, List<CalorimeterHit> hits) {
+            
+        // Store each hit in a set by its cell ID so that it may be
+        // easily acquired later.
+        HashMap<Long, CalorimeterHit> hitMap = new HashMap<Long, CalorimeterHit>();
+        for (CalorimeterHit hit : hits) {
+            hitMap.put(hit.getCellID(), hit);
+        }
+
+        // Remove the last event from the hit buffer and add the new one.
+        hitBuffer.removeLast();
+        hitBuffer.addFirst(hitMap);
+
+        // Run the clustering algorithm on the buffer.
+        List<Cluster> clusterList = getClusters();
+
+        return clusterList;
+    }
+               
+    /**
+     * Sets the number of clock cycles before and after a given cycle that will
+     * be used when checking whether a given hit is a local maximum in both time
+     * and space. Note that a value of
+     * <code>N
+     * </code> indicates that
+     * <code>N</code> clock cycles before and
+     * <code>N</code> clock cycles after will be considered. Thusly, a total of
+     * <code>2N + 1</code> clock cycles will be used total.
+     *
+     * @param clusterWindow - The number of additional clock cycles to include
+     * in the clustering checks. A negative value will be treated as zero.
+     */
+    void setClusterWindow(int clusterWindow) {
+        // The cluster window of must always be at least zero.
+        if (clusterWindow < 0) {
+            this.clusterWindow = 0;
+        }
+        
+        // If the cluster window is non-zero, then store it.
+        else {
+            this.clusterWindow = clusterWindow;
+        }
+    }
+    
+    /**
+     * Sets whether hits should be added to a cluster from the entire
+     * cluster window or just the "future" hits, plus one clock-cycle
+     * of "past" hits as a safety buffer to account for time uncertainty.
+     * 
+     * @param limitClusterRange - <code>true</code> indicates that
+     * the asymmetric clustering window should be used and <code>
+     * false</code> that the symmetric window should be used.
+     */
+    void setLimitClusterRange(boolean limitClusterRange) {
+        this.limitClusterRange = limitClusterRange;
+    }
+    
+    /**
+     * Sets the minimum energy threshold below which hits will not be considered
+     * as cluster centers.
+     *
+     * @param seedEnergyThreshold - The minimum energy for a cluster center.
+     */
+    void setSeedEnergyThreshold(double seedEnergyThreshold) {
+        // A negative energy threshold is non-physical. All thresholds
+        // be at least zero.
+        if (seedEnergyThreshold < 0.0) {
+            this.seedEnergyThreshold = 0.0;
+        } // If the energy threshold is valid, then use it.
+        else {
+            this.seedEnergyThreshold = seedEnergyThreshold;
+        }
+    }     
+}

Modified: java/trunk/ecal-recon/src/test/java/org/hps/recon/ecal/cluster/ClustererTest.java
 =============================================================================
--- java/trunk/ecal-recon/src/test/java/org/hps/recon/ecal/cluster/ClustererTest.java	(original)
+++ java/trunk/ecal-recon/src/test/java/org/hps/recon/ecal/cluster/ClustererTest.java	Fri Jan  9 16:24:39 2015
@@ -35,7 +35,7 @@
  */
 public class ClustererTest extends TestCase {
     
-    static int nEvents = 1000;
+    static int nEvents = 100;
     static final String fileLocation = "http://www.lcsim.org/test/hps-java/MockDataReconTest.slcio";
     File inputFile;
     File testOutputDir;
@@ -48,11 +48,11 @@
             throw new RuntimeException(e);
         }
         
-        // Create test output dir.
+        // Create test output directory.
         testOutputDir = new TestOutputFile(getClass().getSimpleName());
         testOutputDir.mkdir();        
         
-        // Initialize conditions system.
+        // Initialize the conditions system.
         new DatabaseConditionsManager();
         DatabaseConditionsManager.getInstance().setLogLevel(Level.WARNING);
     }
@@ -79,6 +79,14 @@
     
     public void testGTPOnlineClusterer() {
         runClustererTest("GTPOnlineClusterer", null, true, true);
+    }
+    
+    public void testCTPClusterer() {
+        runClustererTest("CTPClusterer", null, true, false);
+    }
+    
+    public void testGTPClusterer() {
+        runClustererTest("GTPClusterer", null, true, false);
     }
     
     /**