<commit_before>/*
 * Last updated on April 23, 2010, 11:40 AM
 *
 *  * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package ch.unizh.ini.jaer.projects.einsteintunnel.sensoryprocessing;
import ch.unizh.ini.jaer.projects.einsteintunnel.sensoryprocessing.BlurringTunnelFilter.NeuronGroup;
import com.sun.opengl.util.GLUT;
import net.sf.jaer.aemonitor.AEConstants;
import net.sf.jaer.chip.*;
import net.sf.jaer.event.*;
import net.sf.jaer.eventprocessing.EventFilter2D;
import net.sf.jaer.eventprocessing.tracking.*;
import net.sf.jaer.graphics.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import javax.media.opengl.*;
import net.sf.jaer.event.EventPacket;
import net.sf.jaer.util.filter.LowpassFilter2d;

/**
 * Tracks moving objects. Modified from BlurringFilter2DTracker.java
 *
 * @author <NAME>/<NAME>
 */
public class BlurringTunnelTracker extends EventFilter2D implements FrameAnnotater,Observer,ClusterTrackerInterface /*, PreferenceChangeListener*/{
    // TODO split out the optical gryo stuff into its own subclass
    // TODO split out the Cluster object as it's own class.

	public int outputSubSample = 5;
	public int lastTimestamp = 0;
	public int flow = 0;
	public short[] xHistogram;
	public ClusterOSCInterface oscInterface = new ClusterOSCInterface();

	/**
     *
     * @return filter description
     */
    public static String getDescription (){
        return "Tracks moving hands, which means it tracks two object at most";
    }
    /**
     * The list of clusters.
     */
    protected java.util.List<Cluster> clusters = new LinkedList();
    /**
     * Blurring filter to getString clusters
     */
    protected BlurringTunnelFilter bfilter;
    /**
     *  clusters to be destroyed
     */
    protected LinkedList<Cluster> pruneList = new LinkedList<Cluster>();
    /**
     * keeps track of absolute cluster number
     */
    protected int clusterCounter = 0;
    /**
     * random
     */
    protected Random random = new Random();
    private int numVelocityPoints = getPrefs().getInt("BlurringTunnelTracker.numVelocityPoints",3);
    private boolean pathsEnabled = getPrefs().getBoolean("BlurringTunnelTracker.pathsEnabled",true);
    private int pathLength = getPrefs().getInt("BlurringTunnelTracker.pathLength",100);
    private boolean useVelocity = getPrefs().getBoolean("BlurringTunnelTracker.useVelocity",true); // enabling this enables both computation and rendering of cluster velocities
    private boolean showClusters = getPrefs().getBoolean("BlurringTunnelTracker.showClusters",false);
    private float velAngDiffDegToNotMerge = getPrefs().getFloat("BlurringTunnelTracker.velAngDiffDegToNotMerge",60.0f);
    private boolean enableMerge = getPrefs().getBoolean("BlurringTunnelTracker.enableMerge",false);
    private boolean showClusterNumber = getPrefs().getBoolean("BlurringTunnelTracker.showClusterNumber",false);
    private boolean showClusterVelocity = getPrefs().getBoolean("BlurringTunnelTracker.showClusterVelocity",false);
    private float velocityVectorScaling = getPrefs().getFloat("BlurringTunnelTracker.velocityVectorScaling",1.0f);
    private final float VELOCITY_VECTOR_SCALING = 1e6f; // to scale rendering of cluster velocityPPT vector, velocityPPT is in pixels/tick=pixels/us so this gives 1 screen pixel per 1 pix/s actual vel
    private boolean showClusterMass = getPrefs().getBoolean("BlurringTunnelTracker.showClusterMass",false);
    private float maximumClusterLifetimeMs = getPrefs().getFloat("BlurringTunnelTracker.maximumClusterLifetimeMs",50.0f);
    private float clusterRadiusLifetimeMs = getPrefs().getFloat("BlurringTunnelTracker.clusterRadiusLifetimeMs",20.0f);
    private int minimumClusterSizePixels = getPrefs().getInt("BlurringTunnelTracker.minimumClusterSizePixels",10);
    private float tauLPFMs = getPrefs().getFloat("BlurringTunnelTracker.tauLPFMs",5.0f);
	private boolean oscEnabled = getPrefs().getBoolean("BlurringTunnelTracker.oscEnabled",true);
    private boolean sendClusterInfo = getPrefs().getBoolean("BlurringTunnelTracker.sendClusterInfo",true);
	private int minClusterAge = getPrefs().getInt("BlurringTunnelTracker.minClusterAge",1000);
	private float activityDecayFactor = getPrefs().getFloat("BlurringTunnelTracker.activityDecayFactor",0.9f);
    private boolean sendActivity = getPrefs().getBoolean("BlurringTunnelTracker.sendActivity",false);
	private float flowThreshold = getPrefs().getFloat("BlurringTunnelTracker.flowThreshold",15f);
	private int inputCount = 0;

    /**
     * Creates a new instance of BlurringFilter2DTracker.
     * @param chip
     */
    public BlurringTunnelTracker (AEChip chip){
        super(chip);
        this.chip = chip;
        initFilter();
        chip.addObserver(this);
        final String sizing = "Sizing", movement = "Movement", lifetime = "Lifetime", disp = "Display",
        global = "Global", update = "Update", logging = "Logging", trjlimit = "Trajectory persistance limit", einstein="Einstein-Tunnel";
        setPropertyTooltip(global,"minimumClusterSizePixels","minimum size of a squre Cluster.");
        setPropertyTooltip(global,"maximumClusterLifetimeMs","upper limit of cluster lifetime. It increases by when the cluster is properly updated. Otherwise, it decreases. When the lifetime becomes zero, the cluster will be expired.");
        setPropertyTooltip(global,"clusterRadiusLifetimeMs","time constant of the cluster radius.");
        setPropertyTooltip(global,"trackSingleCluster","track only one cluster");
        setPropertyTooltip(disp,"pathsEnabled","draws paths of clusters over some window");
        setPropertyTooltip(disp,"pathLength","paths are at most this many packets long");
        setPropertyTooltip(movement,"numVelocityPoints","the number of recent path points (one per packet of events) to use for velocity vector regression");
        setPropertyTooltip(movement,"useVelocity","uses measured cluster velocity to predict future position; vectors are scaled " + String.format("%.1f pix/pix/s",VELOCITY_VECTOR_SCALING / AEConstants.TICK_DEFAULT_US * 1e-6));
        setPropertyTooltip(movement,"tauLPFMs","time constant of LPF");
        setPropertyTooltip(disp,"showClusters","shows clusters");
        setPropertyTooltip(update,"velAngDiffDegToNotMerge","relative angle in degrees of cluster velocity vectors to not merge overlapping clusters");
        setPropertyTooltip(update,"enableMerge","enable merging overlapping clusters");
        setPropertyTooltip(disp,"showClusterVelocity","annotates velocity in pixels/second");
        setPropertyTooltip(disp,"showClusterNumber","shows cluster ID number");
        setPropertyTooltip(disp,"showClusterMass","shows cluster mass");
		setPropertyTooltip(disp,"velocityVectorScaling","scaling of drawn velocity vectors");
        setPropertyTooltip(einstein,"oscEnabled","enables a OSC output of the tracked information");
		setPropertyTooltip(einstein,"minClusterAge","how Old a Cluster has to be for OSC transfer");
		setPropertyTooltip(einstein,"sendClusterInfo","should the informations on the cluster be send out");
		setPropertyTooltip(einstein,"sendActivity","should a histogram of the x-activity be send out");
		setPropertyTooltip(einstein,"activityDecayFactor","how fast should the x-activity histogram decay");
		setPropertyTooltip(einstein,"flowThreshold","threshold of average velocity to become left or right output");

        filterChainSetting();
    }

    /**
     * sets the BlurringFilter2D as a enclosed filter to find cluster
     */
    protected void filterChainSetting (){
        bfilter = new BlurringTunnelFilter(chip);
        bfilter.addObserver(this); // to getString us called during blurring filter iteration at least every updateIntervalUs
        setEnclosedFilter(bfilter);
    }

    /**
     * merge clusters that are too close to each other and that have sufficiently similar velocities (if velocityRatioToNotMergeClusters).
    this must be done interatively, because merging 4 or more clusters feedforward can result in more clusters than
    you start with. each time we merge two clusters, we start over, until there are no more merges on iteration.
    for each cluster, if it is close to another cluster then merge them and start over.
     */
    private void mergeClusters (){
        boolean mergePending;
        Cluster c1 = null;
        Cluster c2 = null;
        do{
            mergePending = false;
            int nc = clusters.size();
            outer:
            for ( int i = 0 ; i < nc ; i++ ){
                c1 = clusters.get(i);
                if(c1.dead)
                    continue;
                for ( int j = i + 1 ; j < nc ; j++ ){
                    c2 = clusters.get(j); // getString the other cluster
                    if(c2.dead)
                        continue;
                    final boolean overlapping = c1.distanceTo(c2) < ( c1.getMaxRadius() + c2.getMaxRadius() );
                    boolean velSimilar = true; // start assuming velocities are similar
                    if ( overlapping && velAngDiffDegToNotMerge > 0 && c1.isVelocityValid() && c2.isVelocityValid() && c1.velocityAngleTo(c2) > velAngDiffDegToNotMerge * Math.PI / 180 ){
                        // if velocities valid for both and velocities are sufficiently different
                        velSimilar = false; // then flag them as different velocities
                    }
                    if ( overlapping && velSimilar ){
                        // if cluster is close to another cluster, merge them
                        // if distance is less than sum of radii merge them and if velAngle < threshold
                        mergePending = true;
                        break outer; // break out of the outer loop
                    }
                }
            }
            if ( mergePending && c1 != null && c2 != null ){
                clusters.add(new Cluster(c1,c2));
                clusters.remove(c1);
                clusters.remove(c2);
            }
        } while ( mergePending );

    }

    @Override
    public void initFilter (){
        clusters.clear();
        clusterCounter = 0;
    }

    /**
     * Prunes out old clusters that don't have support or that should be purged for some other reason.
     */
    private void pruneClusters (){
        clusters.removeAll(pruneList);
        pruneList.clear();
    }

    /**
     * This method updates the list of clusters, pruning and
     * merging clusters and updating positions.
     * It also updates the optical gyro if enabled.
     *
     * @param t the global timestamp of the update.
     */
    private void updateClusterList (int t){
        if(enableMerge)
            mergeClusters();
        pruneClusters();
        updateClusterPaths(t);
    }

    /**
     * Processes the incoming events to track clusters.
     *
     * @param in
     * @return packet of BlurringTunnelTrackerEvent.
     */
    @Override
    public EventPacket<?> filterPacket (EventPacket<?> in){
        if ( in == null ){
            return null;
        }

        if ( enclosedFilter != null ){
            out = enclosedFilter.filterPacket(in);
        } else{
            out = in;
        }

		lastTimestamp = out.getLastTimestamp();

		if(oscEnabled){
            if(inputCount == outputSubSample){
                inputCount = 0;
                sendOSC(in);
            }
            inputCount++;
        }

        return out;
    }

	public void sendOSC(EventPacket<?> in){
        if(sendActivity){
            for(BasicEvent e:in){
                xHistogram[e.x] += 1;
            }
            oscInterface.sendActivity(xHistogram);
            for(int i = 0; i<xHistogram.length; i++){
                xHistogram[i] = (short)(xHistogram[i]*activityDecayFactor);
            }

        }
        if(sendClusterInfo){
			ListIterator clusterItr = clusters.listIterator();
			int flowSum = 0;
            while(clusterItr.hasNext()){
				Cluster clusterToSend = (Cluster)clusterItr.next();
				if (lastTimestamp - clusterToSend.firstUpdateTimestamp > minClusterAge){
					clusterToSend.validForOSC = true;
					flowSum+=clusterToSend.getVelocityPPS().x;
					oscInterface.sendCluster(clusterToSend);
				}
            }
			if(flowSum > flowThreshold){
				flow = 1;
			}else if(flowSum < -flowThreshold){
				flow = -1;
			}else{
				flow = 0;
			}

			oscInterface.sendFlow(flow);
        }
	}

    /**
     * the method that actually does the tracking
     * Tracking is done by selecting the right neuron groups for the next cluster.
     *
     * @param newNeuronGroup : a neuron group detected by BlurringFilter2D
     * @param initialAge
     */
    protected void track (NeuronGroup newNeuronGroup,int initialAge){
        if ( newNeuronGroup.getNumMemberNeurons() == 0 ){
            return;
        }

        // for input neuron group, see which cluster it is closest to and add it to this cluster.
        // if its too far from any cluster, make a new cluster if we can
        Cluster closest = null;<commit_msg>longer flow sampling<commit_after>/*
 * Last updated on April 23, 2010, 11:40 AM
 *
 *  * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package ch.unizh.ini.jaer.projects.einsteintunnel.sensoryprocessing;
import ch.unizh.ini.jaer.projects.einsteintunnel.sensoryprocessing.BlurringTunnelFilter.NeuronGroup;
import com.sun.opengl.util.GLUT;
import net.sf.jaer.aemonitor.AEConstants;
import net.sf.jaer.chip.*;
import net.sf.jaer.event.*;
import net.sf.jaer.eventprocessing.EventFilter2D;
import net.sf.jaer.eventprocessing.tracking.*;
import net.sf.jaer.graphics.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import javax.media.opengl.*;
import net.sf.jaer.event.EventPacket;
import net.sf.jaer.util.filter.LowpassFilter2d;

/**
 * Tracks moving objects. Modified from BlurringFilter2DTracker.java
 *
 * @author <NAME>/<NAME>
 */
public class BlurringTunnelTracker extends EventFilter2D implements FrameAnnotater,Observer,ClusterTrackerInterface /*, PreferenceChangeListener*/{
    // TODO split out the optical gryo stuff into its own subclass
    // TODO split out the Cluster object as it's own class.

	public int outputSubSample = 5;
	public int flowSubSample = 200;
	public int lastTimestamp = 0;
	public int flow = 0;
	public int flowSum = 0;
	public short[] xHistogram;
	public ClusterOSCInterface oscInterface = new ClusterOSCInterface();

	/**
     *
     * @return filter description
     */
    public static String getDescription (){
        return "Tracks moving hands, which means it tracks two object at most";
    }
    /**
     * The list of clusters.
     */
    protected java.util.List<Cluster> clusters = new LinkedList();
    /**
     * Blurring filter to getString clusters
     */
    protected BlurringTunnelFilter bfilter;
    /**
     *  clusters to be destroyed
     */
    protected LinkedList<Cluster> pruneList = new LinkedList<Cluster>();
    /**
     * keeps track of absolute cluster number
     */
    protected int clusterCounter = 0;
    /**
     * random
     */
    protected Random random = new Random();
    private int numVelocityPoints = getPrefs().getInt("BlurringTunnelTracker.numVelocityPoints",3);
    private boolean pathsEnabled = getPrefs().getBoolean("BlurringTunnelTracker.pathsEnabled",true);
    private int pathLength = getPrefs().getInt("BlurringTunnelTracker.pathLength",100);
    private boolean useVelocity = getPrefs().getBoolean("BlurringTunnelTracker.useVelocity",true); // enabling this enables both computation and rendering of cluster velocities
    private boolean showClusters = getPrefs().getBoolean("BlurringTunnelTracker.showClusters",false);
    private float velAngDiffDegToNotMerge = getPrefs().getFloat("BlurringTunnelTracker.velAngDiffDegToNotMerge",60.0f);
    private boolean enableMerge = getPrefs().getBoolean("BlurringTunnelTracker.enableMerge",false);
    private boolean showClusterNumber = getPrefs().getBoolean("BlurringTunnelTracker.showClusterNumber",false);
    private boolean showClusterVelocity = getPrefs().getBoolean("BlurringTunnelTracker.showClusterVelocity",false);
    private float velocityVectorScaling = getPrefs().getFloat("BlurringTunnelTracker.velocityVectorScaling",1.0f);
    private final float VELOCITY_VECTOR_SCALING = 1e6f; // to scale rendering of cluster velocityPPT vector, velocityPPT is in pixels/tick=pixels/us so this gives 1 screen pixel per 1 pix/s actual vel
    private boolean showClusterMass = getPrefs().getBoolean("BlurringTunnelTracker.showClusterMass",false);
    private float maximumClusterLifetimeMs = getPrefs().getFloat("BlurringTunnelTracker.maximumClusterLifetimeMs",50.0f);
    private float clusterRadiusLifetimeMs = getPrefs().getFloat("BlurringTunnelTracker.clusterRadiusLifetimeMs",20.0f);
    private int minimumClusterSizePixels = getPrefs().getInt("BlurringTunnelTracker.minimumClusterSizePixels",10);
    private float tauLPFMs = getPrefs().getFloat("BlurringTunnelTracker.tauLPFMs",5.0f);
	private boolean oscEnabled = getPrefs().getBoolean("BlurringTunnelTracker.oscEnabled",true);
    private boolean sendClusterInfo = getPrefs().getBoolean("BlurringTunnelTracker.sendClusterInfo",true);
	private int minClusterAge = getPrefs().getInt("BlurringTunnelTracker.minClusterAge",1000);
	private float activityDecayFactor = getPrefs().getFloat("BlurringTunnelTracker.activityDecayFactor",0.9f);
    private boolean sendActivity = getPrefs().getBoolean("BlurringTunnelTracker.sendActivity",false);
	private float flowThreshold = getPrefs().getFloat("BlurringTunnelTracker.flowThreshold",15f);
	private int inputCount = 0;

    /**
     * Creates a new instance of BlurringFilter2DTracker.
     * @param chip
     */
    public BlurringTunnelTracker (AEChip chip){
        super(chip);
        this.chip = chip;
        initFilter();
        chip.addObserver(this);
        final String sizing = "Sizing", movement = "Movement", lifetime = "Lifetime", disp = "Display",
        global = "Global", update = "Update", logging = "Logging", trjlimit = "Trajectory persistance limit", einstein="Einstein-Tunnel";
        setPropertyTooltip(global,"minimumClusterSizePixels","minimum size of a squre Cluster.");
        setPropertyTooltip(global,"maximumClusterLifetimeMs","upper limit of cluster lifetime. It increases by when the cluster is properly updated. Otherwise, it decreases. When the lifetime becomes zero, the cluster will be expired.");
        setPropertyTooltip(global,"clusterRadiusLifetimeMs","time constant of the cluster radius.");
        setPropertyTooltip(global,"trackSingleCluster","track only one cluster");
        setPropertyTooltip(disp,"pathsEnabled","draws paths of clusters over some window");
        setPropertyTooltip(disp,"pathLength","paths are at most this many packets long");
        setPropertyTooltip(movement,"numVelocityPoints","the number of recent path points (one per packet of events) to use for velocity vector regression");
        setPropertyTooltip(movement,"useVelocity","uses measured cluster velocity to predict future position; vectors are scaled " + String.format("%.1f pix/pix/s",VELOCITY_VECTOR_SCALING / AEConstants.TICK_DEFAULT_US * 1e-6));
        setPropertyTooltip(movement,"tauLPFMs","time constant of LPF");
        setPropertyTooltip(disp,"showClusters","shows clusters");
        setPropertyTooltip(update,"velAngDiffDegToNotMerge","relative angle in degrees of cluster velocity vectors to not merge overlapping clusters");
        setPropertyTooltip(update,"enableMerge","enable merging overlapping clusters");
        setPropertyTooltip(disp,"showClusterVelocity","annotates velocity in pixels/second");
        setPropertyTooltip(disp,"showClusterNumber","shows cluster ID number");
        setPropertyTooltip(disp,"showClusterMass","shows cluster mass");
		setPropertyTooltip(disp,"velocityVectorScaling","scaling of drawn velocity vectors");
        setPropertyTooltip(einstein,"oscEnabled","enables a OSC output of the tracked information");
		setPropertyTooltip(einstein,"minClusterAge","how Old a Cluster has to be for OSC transfer");
		setPropertyTooltip(einstein,"sendClusterInfo","should the informations on the cluster be send out");
		setPropertyTooltip(einstein,"sendActivity","should a histogram of the x-activity be send out");
		setPropertyTooltip(einstein,"activityDecayFactor","how fast should the x-activity histogram decay");
		setPropertyTooltip(einstein,"flowThreshold","threshold of average velocity to become left or right output");

        filterChainSetting();
    }

    /**
     * sets the BlurringFilter2D as a enclosed filter to find cluster
     */
    protected void filterChainSetting (){
        bfilter = new BlurringTunnelFilter(chip);
        bfilter.addObserver(this); // to getString us called during blurring filter iteration at least every updateIntervalUs
        setEnclosedFilter(bfilter);
    }

    /**
     * merge clusters that are too close to each other and that have sufficiently similar velocities (if velocityRatioToNotMergeClusters).
    this must be done interatively, because merging 4 or more clusters feedforward can result in more clusters than
    you start with. each time we merge two clusters, we start over, until there are no more merges on iteration.
    for each cluster, if it is close to another cluster then merge them and start over.
     */
    private void mergeClusters (){
        boolean mergePending;
        Cluster c1 = null;
        Cluster c2 = null;
        do{
            mergePending = false;
            int nc = clusters.size();
            outer:
            for ( int i = 0 ; i < nc ; i++ ){
                c1 = clusters.get(i);
                if(c1.dead)
                    continue;
                for ( int j = i + 1 ; j < nc ; j++ ){
                    c2 = clusters.get(j); // getString the other cluster
                    if(c2.dead)
                        continue;
                    final boolean overlapping = c1.distanceTo(c2) < ( c1.getMaxRadius() + c2.getMaxRadius() );
                    boolean velSimilar = true; // start assuming velocities are similar
                    if ( overlapping && velAngDiffDegToNotMerge > 0 && c1.isVelocityValid() && c2.isVelocityValid() && c1.velocityAngleTo(c2) > velAngDiffDegToNotMerge * Math.PI / 180 ){
                        // if velocities valid for both and velocities are sufficiently different
                        velSimilar = false; // then flag them as different velocities
                    }
                    if ( overlapping && velSimilar ){
                        // if cluster is close to another cluster, merge them
                        // if distance is less than sum of radii merge them and if velAngle < threshold
                        mergePending = true;
                        break outer; // break out of the outer loop
                    }
                }
            }
            if ( mergePending && c1 != null && c2 != null ){
                clusters.add(new Cluster(c1,c2));
                clusters.remove(c1);
                clusters.remove(c2);
            }
        } while ( mergePending );

    }

    @Override
    public void initFilter (){
        clusters.clear();
        clusterCounter = 0;
    }

    /**
     * Prunes out old clusters that don't have support or that should be purged for some other reason.
     */
    private void pruneClusters (){
        clusters.removeAll(pruneList);
        pruneList.clear();
    }

    /**
     * This method updates the list of clusters, pruning and
     * merging clusters and updating positions.
     * It also updates the optical gyro if enabled.
     *
     * @param t the global timestamp of the update.
     */
    private void updateClusterList (int t){
        if(enableMerge)
            mergeClusters();
        pruneClusters();
        updateClusterPaths(t);
    }

    /**
     * Processes the incoming events to track clusters.
     *
     * @param in
     * @return packet of BlurringTunnelTrackerEvent.
     */
    @Override
    public EventPacket<?> filterPacket (EventPacket<?> in){
        if ( in == null ){
            return null;
        }

        if ( enclosedFilter != null ){
            out = enclosedFilter.filterPacket(in);
        } else{
            out = in;
        }

		lastTimestamp = out.getLastTimestamp();

		if(oscEnabled){
            if(inputCount % outputSubSample == 0){
                sendOSC(in);
            }
			if(inputCount % flowSubSample == 0){
				if(flowSum > flowThreshold){
					flow = 1;
				}else if(flowSum < -flowThreshold){
					flow = -1;
				}else{
					flow = 0;
				}
				flowSum = 0;
            }
			if(inputCount >= outputSubSample && inputCount >= flowSubSample) inputCount = 0;
            inputCount++;
        }

        return out;
    }

	public void sendOSC(EventPacket<?> in){
        if(sendActivity){
            for(BasicEvent e:in){
                xHistogram[e.x] += 1;
            }
            oscInterface.sendActivity(xHistogram);
            for(int i = 0; i<xHistogram.length; i++){
                xHistogram[i] = (short)(xHistogram[i]*activityDecayFactor);
            }

        }
        if(sendClusterInfo){
			ListIterator clusterItr = clusters.listIterator();
            while(clusterItr.hasNext()){
				Cluster clusterToSend = (Cluster)clusterItr.next();
				if (lastTimestamp - clusterToSend.firstUpdateTimestamp > minClusterAge){
					clusterToSend.validForOSC = true;
					flowSum+=clusterToSend.getVelocityPPS().x;
					oscInterface.sendCluster(clusterToSend);
				}
            }

			oscInterface.sendFlow(flow);
        }
	}

    /**
     * the method that actually does the tracking
     * Tracking is done by selecting the right neuron groups for the next cluster.
     *
     * @param newNeuronGroup : a neuron group detected by BlurringFilter2D
     * @param initialAge
     */
    protected void track (NeuronGroup newNeuronGroup,int initialAge){
        if ( newNeuronGroup.getNumMemberNeurons() == 0 ){
            return;
        }

        // for input neuron group, see which cluster it is closest to and add it to this cluster.
        // if its too far from any cluster, make a new cluster if we can
        Cluster closest = null;
{'fraction_non_alphanumeric': 0.09500322494972872, 'fraction_numerical': 0.007208711158326061, 'mean_word_length': 3.0259660913395448, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2566475', 'n_tokens_mistral': 7365, 'n_tokens_neox': 6988, 'n_words': 1990}