tr.gov.ulakbim.jDenetX.streams.clustering.RandomRBFGeneratorEvents.java Source code

Java tutorial

Introduction

Here is the source code for tr.gov.ulakbim.jDenetX.streams.clustering.RandomRBFGeneratorEvents.java

Source

/*
 *    RandomRBFGenerator.java
 *    Copyright (C) 2007 University of Waikato, Hamilton, New Zealand
 *    @author Richard Kirkby (rkirkby@cs.waikato.ac.nz)
 *
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package tr.gov.ulakbim.jDenetX.streams.clustering;

import tr.gov.ulakbim.jDenetX.cluster.Clustering;
import tr.gov.ulakbim.jDenetX.cluster.SphereCluster;
import tr.gov.ulakbim.jDenetX.core.AutoExpandVector;
import tr.gov.ulakbim.jDenetX.core.InstancesHeader;
import tr.gov.ulakbim.jDenetX.core.ObjectRepository;
import tr.gov.ulakbim.jDenetX.gui.visualization.DataPoint;
import tr.gov.ulakbim.jDenetX.options.FloatOption;
import tr.gov.ulakbim.jDenetX.options.IntOption;
import tr.gov.ulakbim.jDenetX.streams.InstanceStream;
import tr.gov.ulakbim.jDenetX.tasks.TaskMonitor;
import weka.core.*;

import java.util.*;

public class RandomRBFGeneratorEvents extends ClusteringStream {
    private transient Vector listeners;

    private static final long serialVersionUID = 1L;

    public IntOption modelRandomSeedOption = new IntOption("modelRandomSeed", 'm',
            "Seed for random generation of model.", 1);

    public IntOption instanceRandomSeedOption = new IntOption("instanceRandomSeed", 'i',
            "Seed for random generation of instances.", 1);

    public IntOption numClusterOption = new IntOption("numCluster", 'K',
            "The average number of centroids in the model.", 4, 1, Integer.MAX_VALUE);

    public IntOption numClusterRangeOption = new IntOption("numClusterRange", 'k',
            "Deviation of the number of centroids in the model.", 3, 1, Integer.MAX_VALUE);

    public FloatOption kernelRadiiOption = new FloatOption("kernelRadius", 'R',
            "The average radii of the centroids in the model.", 0.05, 0, 1);

    public FloatOption kernelRadiiRangeOption = new FloatOption("kernelRadiusRange", 'r',
            "Deviation of average radii of the centroids in the model.", 0, 0, 1);

    public FloatOption densityRangeOption = new FloatOption("densityRange", 'd',
            "Offset of the average weight a cluster has. Value of 0 means all cluster "
                    + "contain the same amount of points.",
            0, 0, 1);

    public IntOption speedOption = new IntOption("speed", 'V',
            "Kernels move a predefined distance of 0.01 every X points", 100, 1, Integer.MAX_VALUE);

    public IntOption speedRangeOption = new IntOption("speedRange", 'v', "Speed/Velocity point offset", 0, 0,
            Integer.MAX_VALUE);

    public FloatOption noiseLevelOption = new FloatOption("noiseLevel", 'N', "Noise level", 0.1, 0, 1);

    public IntOption eventFrequencyOption = new IntOption("eventFrequency", 'E', "Event frequency", 15000, 0,
            Integer.MAX_VALUE);

    public FloatOption eventMergeWeightOption = new FloatOption("eventMergeWeight", 'M', "", 0.5, 0, 1);

    public FloatOption eventSplitWeightOption = new FloatOption("eventSplitWeight", 'P',
            "Influences the probablity of SplitClusterChange events relative to the total sum of all event-weights."
                    + "SplitClusterChange Events will split a cluster into two clusters.",
            0.5, 0, 1);

    //    public FloatOption eventSizeWeightOption = new FloatOption("eventSizeWeight", 'S',
    //                    "Influences the probablity of SizeClusterChange events relative to the total sum of all event-weights." +
    //                    "SizeClusterChange Events will increase/decrease the clusters radius.", 0.5, 0, 1);
    //
    //    public FloatOption eventDensityWeightOption = new FloatOption("eventDensityWeight", 'D',
    //                    "Influences the probablity of DensityClusterChange events relative to the total sum of all event-weights." +
    //                    "DensityClusterChange Events will increase/decrease the amount of points contained by a cluster.", 0.5, 0, 1);

    private double merge_threshold = 0.7;
    private int kernelMovePointFrequency = 10;
    private double maxDistanceMoveThresholdByStep = 0.01;
    private int maxOverlapFitRuns = 50;
    private double eventFrequencyRange = 0.25;
    //double test = (2.0/5.0) + (2.0/5.0) - 0.6;

    private boolean debug = true;

    private AutoExpandVector<GeneratorCluster> kernels;
    protected Random instanceRandom;
    protected InstancesHeader streamHeader;
    private int numGeneratedInstances;
    private int numActiveKernels;
    private int nextEventCounter;
    private int nextEventChoice;
    private int clusterIdCounter;
    private GeneratorCluster mergeClusterA;
    private GeneratorCluster mergeClusterB;

    private class GeneratorCluster {
        //TODO: points is redundant to microclusterpoints, we need to come 
        //up with a good strategie that microclusters get updated and 
        //rebuild if needed. Idea: Sort microclusterpoints by timestamp and let 
        // microclusterdecay hold the timestamp for when the last point in a 
        //micro cluster gets kicked then we rebuild... or maybe not... could be
        //same as searching for point to be kicked. more likely is we rebuild 
        //fewer times then insert.

        SphereCluster generator;
        int kill = -1;
        boolean merging = false;
        double[] moveVector;
        int totalMovementSteps;
        int currentMovementSteps;

        LinkedList<DataPoint> points = new LinkedList<DataPoint>();
        ArrayList<SphereCluster> microClusters = new ArrayList<SphereCluster>();
        ArrayList<ArrayList<DataPoint>> microClustersPoints = new ArrayList();
        ArrayList<Integer> microClustersDecay = new ArrayList();

        public GeneratorCluster(int label) {
            boolean outofbounds = true;
            int tryCounter = 0;
            while (outofbounds && tryCounter < maxOverlapFitRuns) {
                tryCounter++;
                outofbounds = false;
                double[] center = new double[numAttsOption.getValue()];
                double radius = kernelRadiiOption.getValue() + (instanceRandom.nextBoolean() ? -1 : 1)
                        * kernelRadiiRangeOption.getValue() * instanceRandom.nextDouble();
                while (radius <= 0) {
                    radius = kernelRadiiOption.getValue() + (instanceRandom.nextBoolean() ? -1 : 1)
                            * kernelRadiiRangeOption.getValue() * instanceRandom.nextDouble();
                }
                for (int j = 0; j < numAttsOption.getValue(); j++) {
                    center[j] = instanceRandom.nextDouble();
                    if (center[j] - radius < 0 || center[j] + radius > 1) {
                        outofbounds = true;
                        break;
                    }
                }
                generator = new SphereCluster(center, radius);
            }
            if (tryCounter < maxOverlapFitRuns) {
                generator.setId(label);
                double avgWeight = 1.0 / numClusterOption.getValue();
                double weight = avgWeight + avgWeight * densityRangeOption.getValue() * instanceRandom.nextDouble();
                generator.setWeight(weight);
                setDesitnation(null, 0);
            } else {
                generator = null;
                kill = 0;
                System.out.println("Tried " + maxOverlapFitRuns + " times to create kernel. Reduce average radii.");
            }
        }

        public GeneratorCluster(int label, SphereCluster cluster) {
            this.generator = cluster;
            cluster.setId(label);
            setDesitnation(null, 0);
        }

        public int getWorkID() {
            for (int c = 0; c < kernels.size(); c++) {
                if (kernels.get(c).equals(this))
                    return c;
            }
            return -1;
        }

        private void updateKernel() {
            if (kill == 0) {
                kernels.remove(this);
            }
            if (kill > 0) {
                kill--;
            }
            //we could be lot more precise if we would keep track of timestamps of points
            //then we could remove all old points and rebuild the cluster on up to date point base
            //BUT worse the effort??? so far we just want to avoid overlap with this, so its more
            //konservative as needed. Only needs to change when we need a thighter representation
            for (int m = 0; m < microClusters.size(); m++) {
                if (numGeneratedInstances - microClustersDecay.get(m) > decayHorizonOption.getValue()) {
                    microClusters.remove(m);
                    microClustersPoints.remove(m);
                    microClustersDecay.remove(m);
                }
            }

            if (!points.isEmpty()
                    && numGeneratedInstances - points.getFirst().getTimestamp() >= decayHorizonOption.getValue()) {
                //                if(debug)
                //                    System.out.println("Cleaning up macro cluster "+generator.getId());
                points.removeFirst();
            }

        }

        private void addInstance(Instance instance) {
            DataPoint point = new DataPoint(instance, numGeneratedInstances);
            points.add(point);

            int minMicroIndex = -1;
            double minHullDist = Double.MAX_VALUE;
            boolean inserted = false;
            //we favour more recently build clusters so we can remove earlier cluster sooner
            for (int m = microClusters.size() - 1; m >= 0; m--) {
                SphereCluster micro = microClusters.get(m);
                double hulldist = micro.getCenterDistance(point) - micro.getRadius();
                //point fits into existing cluster
                if (hulldist <= 0) {
                    microClustersPoints.get(m).add(point);
                    microClustersDecay.set(m, numGeneratedInstances);
                    inserted = true;
                    break;
                }
                //if not, check if its at least the closest cluster
                else {
                    if (hulldist < minHullDist) {
                        minMicroIndex = m;
                        minHullDist = hulldist;
                    }
                }
            }
            //Reseting index choice for alternative cluster building
            int alt = 1;
            if (alt == 1)
                minMicroIndex = -1;
            if (!inserted) {
                //add to closest cluster and expand cluster
                if (minMicroIndex != -1) {
                    microClustersPoints.get(minMicroIndex).add(point);
                    //we should keep the miniball instances and just check in
                    //new points instead of rebuilding the whole thing
                    SphereCluster s = new SphereCluster(microClustersPoints.get(minMicroIndex),
                            numAttsOption.getValue());
                    //check if current microcluster is bigger then generating cluster
                    if (s.getRadius() > generator.getRadius()) {
                        //remove previously added point
                        microClustersPoints.get(minMicroIndex)
                                .remove(microClustersPoints.get(minMicroIndex).size() - 1);
                        minMicroIndex = -1;
                    } else {
                        microClusters.set(minMicroIndex, s);
                        microClustersDecay.set(minMicroIndex, numGeneratedInstances);
                    }
                }
                //minMicroIndex might have been reset above
                //create new micro cluster
                if (minMicroIndex == -1) {
                    ArrayList<DataPoint> microPoints = new ArrayList<DataPoint>();
                    microPoints.add(point);
                    SphereCluster s;
                    if (alt == 0)
                        s = new SphereCluster(microPoints, numAttsOption.getValue());
                    else
                        s = new SphereCluster(generator.getCenter(), generator.getRadius(), 1);

                    microClusters.add(s);
                    microClustersPoints.add(microPoints);
                    microClustersDecay.add(numGeneratedInstances);
                    int id = 0;
                    while (id < kernels.size()) {
                        if (kernels.get(id) == this)
                            break;
                        id++;
                    }
                    s.setGroundTruth(id);
                }
            }

        }

        private void move() {
            if (currentMovementSteps < totalMovementSteps) {
                currentMovementSteps++;
                if (moveVector == null) {
                    return;
                } else {
                    double[] center = generator.getCenter();
                    boolean outofbounds = true;
                    while (outofbounds) {
                        double radius = generator.getRadius();
                        outofbounds = false;
                        center = generator.getCenter();
                        for (int d = 0; d < center.length; d++) {
                            center[d] += moveVector[d];
                            if (center[d] - radius < 0 || center[d] + radius > 1) {
                                outofbounds = true;
                                setDesitnation(null, 0);
                                break;
                            }
                        }
                    }
                    generator.setCenter(center);
                }
            } else {
                if (!merging) {
                    setDesitnation(null, 0);
                }
            }
        }

        void setDesitnation(double[] destination, int steps) {

            if (destination == null) {
                destination = new double[numAttsOption.getValue()];
                for (int j = 0; j < numAttsOption.getValue(); j++) {
                    destination[j] = instanceRandom.nextDouble();
                }
            }
            double[] center = generator.getCenter();
            int dim = center.length;

            double[] v = new double[dim];

            for (int d = 0; d < dim; d++) {
                v[d] = destination[d] - center[d];
            }
            setMoveVector(v, steps);
        }

        void setMoveVector(double[] vector, int steps) {
            moveVector = vector;
            int speedInPoints = speedOption.getValue();
            if (speedRangeOption.getValue() > 0)
                speedInPoints += (instanceRandom.nextBoolean() ? -1 : 1)
                        * instanceRandom.nextInt(speedRangeOption.getValue());
            if (speedInPoints < 1)
                speedInPoints = speedOption.getValue();

            double length = 0;
            for (int d = 0; d < moveVector.length; d++) {
                length += Math.pow(vector[d], 2);
            }

            totalMovementSteps = (int) (length / maxDistanceMoveThresholdByStep * speedInPoints);
            for (int d = 0; d < moveVector.length; d++) {
                moveVector[d] /= (double) totalMovementSteps;
            }

            currentMovementSteps = 0;
            //            if(debug){
            //                System.out.println("Setting new direction for C"+generator.getId()+": distance "
            //                        +Math.sqrt(length)+" in "+totalMovementSteps+" steps");
            //            }
        }

        private String tryMerging(GeneratorCluster merge) {
            String message = "";
            if (generator.overlapRadiusDegree(merge.generator) > merge_threshold) {
                SphereCluster mcluster = merge.generator;
                double radius = Math.max(generator.getRadius(), mcluster.getRadius());
                generator.combine(mcluster);

                //                //adjust radius, get bigger and bigger with high dim data
                generator.setRadius(radius);
                //                double[] center = generator.getCenter();
                //                double[] mcenter = mcluster.getCenter();
                //                double weight = generator.getWeight();
                //                double mweight = generator.getWeight();
                ////                for (int i = 0; i < center.length; i++) {
                ////                    center[i] = (center[i] * weight + mcenter[i] * mweight) / (mweight + weight);
                ////                }
                //                generator.setWeight(weight + mweight);
                message = "Clusters merging: " + mergeClusterB.generator.getId() + " into "
                        + mergeClusterA.generator.getId();

                //clean up and restet merging stuff
                //mark kernel so it gets killed when it doesn't contain any more instances
                merge.kill = decayHorizonOption.getValue();
                //set weight to 0 so no new instances will be created in the cluster
                mcluster.setWeight(0.0);
                normalizeWeights();
                numActiveKernels--;
                mergeClusterB = mergeClusterA = null;
                merging = false;
            }
            return message;
        }

        private String splitKernel() {

            //todo radius range
            double radius = kernelRadiiOption.getValue();
            double avgWeight = 1.0 / numClusterOption.getValue();
            double weight = avgWeight + avgWeight * densityRangeOption.getValue() * instanceRandom.nextDouble();
            SphereCluster spcluster = null;

            double[] center = generator.getCenter();
            spcluster = new SphereCluster(center, radius, weight);

            if (spcluster != null) {
                GeneratorCluster gc = new GeneratorCluster(clusterIdCounter++, spcluster);
                kernels.add(gc);
                normalizeWeights();
                numActiveKernels++;
                return "Split from Kernel " + generator.getId();
            } else {
                System.out.println("Tried to split new kernel from C" + generator.getId()
                        + ". Not enough room for new cluster, decrease average radii, number of clusters or enable overlap.");
                return "";
            }

        }
    }

    public RandomRBFGeneratorEvents() {

    }

    public InstancesHeader getHeader() {
        return streamHeader;
    }

    public long estimatedRemainingInstances() {
        return -1;
    }

    public boolean hasMoreInstances() {
        return true;
    }

    public boolean isRestartable() {
        return true;
    }

    @Override
    public void prepareForUseImpl(TaskMonitor monitor, ObjectRepository repository) {
        monitor.setCurrentActivity("Preparing random RBF...", -1.0);
        generateHeader();
        restart();
    }

    public void restart() {
        instanceRandom = new Random(instanceRandomSeedOption.getValue());
        nextEventCounter = eventFrequencyOption.getValue();
        nextEventChoice = instanceRandom.nextInt(2);
        numActiveKernels = 0;
        numGeneratedInstances = 0;
        clusterIdCounter = 0;
        mergeClusterA = mergeClusterB = null;
        kernels = new AutoExpandVector<GeneratorCluster>();

        initKernels();
    }

    protected void generateHeader() {
        FastVector attributes = new FastVector();
        for (int i = 0; i < this.numAttsOption.getValue(); i++) {
            attributes.addElement(new Attribute("att" + (i + 1)));
        }
        FastVector classLabels = new FastVector();
        for (int i = 0; i < this.numClusterOption.getValue(); i++) {
            classLabels.addElement("class" + (i + 1));
        }
        attributes.addElement(new Attribute("class", classLabels));
        streamHeader = new InstancesHeader(
                new Instances(getCLICreationString(InstanceStream.class), attributes, 0));
        streamHeader.setClassIndex(streamHeader.numAttributes() - 1);
    }

    protected void initKernels() {
        for (int i = 0; i < numClusterOption.getValue(); i++) {
            kernels.add(new GeneratorCluster(clusterIdCounter));
            numActiveKernels++;
            clusterIdCounter++;
        }
        normalizeWeights();
        //updateOverlaps();
    }

    public Instance nextInstance() {
        numGeneratedInstances++;
        eventScheduler();

        //make room for thge classlabel
        double[] values_new = new double[numAttsOption.getValue() + 1];
        double[] values = null;
        int clusterChoice = -1;

        if (instanceRandom.nextDouble() > noiseLevelOption.getValue()) {
            clusterChoice = chooseWeightedElement();
            values = kernels.get(clusterChoice).generator.sample(instanceRandom).toDoubleArray();
        } else {
            //get ranodm noise point
            values = getNewSample();
        }

        if (Double.isNaN(values[0])) {
            System.out.println("Instance corrupted:" + numGeneratedInstances);
        }
        System.arraycopy(values, 0, values_new, 0, values.length);

        Instance inst = new DenseInstance(1.0, values_new);
        inst.setDataset(getHeader());
        if (clusterChoice == -1) {
            inst.setClassValue(-1);
        } else {
            inst.setClassValue(kernels.get(clusterChoice).generator.getId());
            //Do we need micro cluster representation if have overlapping clusters?
            //if(!overlappingOption.isSet())
            kernels.get(clusterChoice).addInstance(inst);
        }
        //        System.out.println(numGeneratedInstances+": Overlap is"+updateOverlaps());

        return inst;
    }

    public Clustering getGeneratingClusters() {
        Clustering clustering = new Clustering();
        for (int c = 0; c < kernels.size(); c++) {
            clustering.add(kernels.get(c).generator);
        }
        return clustering;
    }

    public Clustering getClustering() {
        Clustering clustering = new Clustering();
        int id = 0;

        for (int c = 0; c < kernels.size(); c++) {
            for (int m = 0; m < kernels.get(c).microClusters.size(); m++) {
                kernels.get(c).microClusters.get(m).setId(id);
                clustering.add(kernels.get(c).microClusters.get(m));
                id++;
            }
        }

        //System.out.println("numMicroKernels "+clustering.size());
        return clustering;
    }

    /**
     * ************************* EVENTS *****************************************
     */
    private void eventScheduler() {
        for (int i = 0; i < kernels.size(); i++) {
            kernels.get(i).updateKernel();
        }

        nextEventCounter--;
        //only move kernels every 10 points, performance reasons????
        //should this be randomized as well???
        if (nextEventCounter % kernelMovePointFrequency == 0) {
            //move kernels
            for (int i = 0; i < kernels.size(); i++) {
                kernels.get(i).move();
                //overlapControl();
            }
        }

        String type = "";
        String message = "";
        switch (nextEventChoice) {
        case 0:
            if (nextEventCounter <= 0) {
                if (numActiveKernels < numClusterOption.getValue() + numClusterRangeOption.getValue()) {
                    type = "Split";
                    message = splitKernel();
                    message += " -> numKernels = " + numActiveKernels;
                } else {
                    nextEventChoice = -1;
                }
            }
            break;
        case 1:
            if (numActiveKernels > numClusterOption.getValue() - numClusterRangeOption.getValue()) {
                message = mergeKernels(false);
                type = "Merge";
                if (!message.equals(""))
                    message += " -> numKernels = " + numActiveKernels;
            } else {
                nextEventChoice = -1;
            }
            break;
        case 2:
            if (nextEventCounter <= 0) {
                message = changeWeight(true);
                type = "Increase Weight";
            }
            break;
        case 3:
            if (nextEventCounter <= 0) {
                message = changeWeight(false);
                type = "Decrease Weight";
            }
            break;
        case 4:
            if (nextEventCounter <= 0) {
                message = changeRadius(true);
                type = "Increase Radius";
            }
            break;
        case 5:
            if (nextEventCounter <= 0) {
                message = changeRadius(false);
                type = "Decrease Radius";
            }
            break;
        }
        if ((nextEventCounter <= 0 && !message.isEmpty()) || nextEventChoice == -1) {
            nextEventCounter = (int) (eventFrequencyOption.getValue() + (instanceRandom.nextBoolean() ? -1 : 1)
                    * eventFrequencyOption.getValue() * eventFrequencyRange * instanceRandom.nextDouble());
            nextEventChoice = instanceRandom.nextInt(2);
        }
        if (!message.isEmpty())
            fireClusterChange(numGeneratedInstances, type, message);
    }

    private String changeWeight(boolean increase) {
        double changeRate = 0.1;
        int id = instanceRandom.nextInt(kernels.size());
        while (kernels.get(id).kill != -1)
            id = instanceRandom.nextInt(kernels.size());

        int sign = 1;
        if (!increase)
            sign = -1;
        double weight_old = kernels.get(id).generator.getWeight();
        double delta = sign * numActiveKernels * instanceRandom.nextDouble() * changeRate;
        kernels.get(id).generator.setWeight(weight_old + delta);

        normalizeWeights();

        String message;
        if (increase)
            message = "Increase ";
        else
            message = "Decrease ";
        message += " weight on Cluster " + id + " from " + weight_old + " to " + (weight_old + delta);
        return message;

    }

    private String changeRadius(boolean increase) {
        double maxChangeRate = 0.1;
        int id = instanceRandom.nextInt(kernels.size());
        while (kernels.get(id).kill != -1)
            id = instanceRandom.nextInt(kernels.size());

        int sign = 1;
        if (!increase)
            sign = -1;

        double r_old = kernels.get(id).generator.getRadius();
        double r_new = r_old + sign * r_old * instanceRandom.nextDouble() * maxChangeRate;
        if (r_new >= 0.5)
            return "Radius to big";
        kernels.get(id).generator.setRadius(r_new);

        String message;
        if (increase)
            message = "Increase ";
        else
            message = "Decrease ";
        message += " radius on Cluster " + id + " from " + r_old + " to " + r_new;
        return message;
    }

    private String splitKernel() {
        int id = instanceRandom.nextInt(kernels.size());
        while (kernels.get(id).kill != -1)
            id = instanceRandom.nextInt(kernels.size());

        String message = kernels.get(id).splitKernel();

        return message;
        //TODO generateHeader(); does that do anything? Ref on dataset in instances?
    }

    private String mergeKernels(boolean reset) {
        if (numActiveKernels > 1 && ((mergeClusterA == null && mergeClusterB == null) || reset)) {
            //            if(reset){
            //                System.out.println("Reset merging, wasn't possible to merge C"+mergeClusterA+" and C"+mergeClusterB);
            //                if(mergeClusterA!=-1)
            //                    kernels.get(mergeClusterA).merging = false;
            //                if(mergeClusterA!=-1)
            //                    kernels.get(mergeClusterB).merging = false;
            //                mergeClusterA = mergeClusterB = -1;
            //
            //            }
            //choose clusters to merge
            mergeClusterA = kernels.get(instanceRandom.nextInt(kernels.size()));
            while (mergeClusterA.kill != -1)
                mergeClusterA = kernels.get(instanceRandom.nextInt(kernels.size()));

            mergeClusterB = mergeClusterA;
            while (mergeClusterB == mergeClusterA || mergeClusterB.kill != -1) {
                mergeClusterB = kernels.get(instanceRandom.nextInt(kernels.size()));
            }
            boolean outofbound = true;
            double[] merge_point = new double[numAttsOption.getValue()];
            double maxradius = Math.max(mergeClusterA.generator.getRadius(), mergeClusterB.generator.getRadius());

            int counter = maxOverlapFitRuns;
            while (outofbound && counter > 0) {
                counter--;
                outofbound = false;
                for (int j = 0; j < numAttsOption.getValue(); j++) {
                    merge_point[j] = instanceRandom.nextDouble();
                    if (merge_point[j] - maxradius < 0 || merge_point[j] + maxradius > 1) {
                        outofbound = true;
                        break;
                    }
                }
            }
            if (counter <= 0)
                return "";

            mergeClusterA.merging = true;
            mergeClusterB.merging = true;
            mergeClusterA.setDesitnation(merge_point, nextEventCounter);
            mergeClusterB.setDesitnation(merge_point, nextEventCounter);
            if (debug)
                System.out.println(
                        "Try to merge cluster " + mergeClusterA.getWorkID() + " into " + mergeClusterB.getWorkID()
                                + " at " + Arrays.toString(merge_point) + " time " + numGeneratedInstances);

            return "";
        }

        if (mergeClusterA != null && mergeClusterB != null) {

            //movekernels will move the kernels close to each other,
            //we just need to check and merge here if they are close enough
            return mergeClusterA.tryMerging(mergeClusterB);
        }

        return "";
    }

    /**
     * ********************** TOOLS *************************************
     */

    public void getDescription(StringBuilder sb, int indent) {
        // TODO Auto-generated method stub

    }

    private double[] getNewSample() {
        double[] sample = new double[numAttsOption.getValue()];
        for (int j = 0; j < numAttsOption.getValue(); j++) {
            sample[j] = instanceRandom.nextDouble();
        }
        return sample;
    }

    private int chooseWeightedElement() {
        double r = instanceRandom.nextDouble();

        // Determine index of choosen element
        int i = 0;
        while (r > 0.0) {
            r -= kernels.get(i).generator.getWeight();
            i++;
        }
        --i; // Overcounted once
        //System.out.println(i);
        return i;
    }

    private void normalizeWeights() {
        double sumWeights = 0.0;
        for (int i = 0; i < kernels.size(); i++) {
            sumWeights += kernels.get(i).generator.getWeight();
        }
        for (int i = 0; i < kernels.size(); i++) {
            kernels.get(i).generator.setWeight(kernels.get(i).generator.getWeight() / sumWeights);
        }
    }

    /*************** EVENT Listener *********************/
    // should go into the superclass of the generator, create new one for cluster streams?

    /**
     * Add a listener
     */
    synchronized public void addClusterChangeListener(ClusterEventListener l) {
        if (listeners == null)
            listeners = new Vector();
        listeners.addElement(l);
    }

    /**
     * Remove a listener
     */
    synchronized public void removeClusterChangeListener(ClusterEventListener l) {
        if (listeners == null)
            listeners = new Vector();
        listeners.removeElement(l);
    }

    /**
     * Fire a ClusterChangeEvent to all registered listeners
     */
    protected void fireClusterChange(long timestamp, String type, String message) {
        // if we have no listeners, do nothing...
        if (listeners != null && !listeners.isEmpty()) {
            // create the event object to send
            ClusterEvent event = new ClusterEvent(this, timestamp, type, message);

            // make a copy of the listener list in case
            //   anyone adds/removes listeners
            Vector targets;
            synchronized (this) {
                targets = (Vector) listeners.clone();
            }

            // walk through the listener list and
            //   call the sunMoved method in each
            Enumeration e = targets.elements();
            while (e.hasMoreElements()) {
                ClusterEventListener l = (ClusterEventListener) e.nextElement();
                l.changeCluster(event);

            }
        }
    }

    @Override
    public String getPurposeString() {
        return "Generates a random radial basis function stream.";
    }

    public String getParameterString() {
        return "";
    }

}