package nn_assignment;

public class KohonenMap
{
	//Globals to hold the size of the map and the depth of the input data
	public int mapWidth;
	public int mapHeight;
	public int numberOfInputs;

	//Input label and the input array are used to input data into the class
	// for training
	public double inputLabel = 0.0;
	public double[] inputs;

	//Weights is an many dimentional array that will hold all of our nodes
	// as they are being trained
	public double[][][] weights = null;

	//Label is an array of the same dimentions as our weights array which will
	// hold the corresponding label for each point (just a sloppy want to track
	// what type each node is
	public double[][][] labels = null;

	//Globals to hold the winning nodes each training cycle (just sloppy globals
	// to stop me passing values between functions
	public int winnerXPos = 0;
	public int winnerYPos = 0;

	//Learning rates will decrease as the program trains so that the map settles
	// so we need to track what they are currently and what they are each time
	// we reset the map and how fast to decay and at what rate the decay should
	// stop (minimum value)
	public double initialLearningRate = 0.1;
	public double currentLearningRate = initialLearningRate;
	public double learningRateDecay   = 0.999;
	public double minimumLearningRate = 0.01;

	//UPdate radius is used to work how how many weight nodes we should affect
	// when we change weights. It will degrade over time as well as the map
	// settles. We also set what the minimum update radius is (we always change
	// at least one (1) node.
	public double initialUpdateRadius = .3;
	public double currentUpdateRadius = .3;
	public double updateRadiusDecay   = 0.999;
	public double minimumUpdateRadius = 1.0;

	//A global to dictate how the map will work as far as borders go
	public boolean wrapAroundMapBorders = false;


	//Simple constuctor, just set sizes and input depths
	public KohonenMap(int init_numberOfInputs, int init_mapWidth, int init_mapHeight)
	{
		numberOfInputs  = init_numberOfInputs;
		mapWidth        = init_mapWidth;
		mapHeight       = init_mapHeight;
	}

	//Initialise the map, create matricies for the weights and labels
	// and get an array ready for out input values
	public void init()
	{
		inputs  = new double[numberOfInputs];
		weights = new double[mapWidth][mapHeight][numberOfInputs];
		labels  = new double[mapWidth][mapHeight][1];

		//Set the inital update radius to a percentage of the map size
		// (arbitary value because its getting late)
		initialUpdateRadius = mapWidth * initialUpdateRadius;

		//Set the working update radius to our initial value
		currentUpdateRadius = initialUpdateRadius;

		//Go ahead and create our weights arrays
		resetMap();
	}


	//This method will go through and reset the initial learning rates/radiuss
	// and more importantly go and put nice random values in our map (to begin
	// with) around about the center of the map and set all of our labels matrix
	// to 0.0;
	public void resetMap()
	{
		//Setup the inital learning rate and radius
		currentLearningRate = initialLearningRate;
		currentUpdateRadius = initialUpdateRadius;

		//Iterate through the whole map and set random values for each
		// node. Random values are restricted to points in a (fairly) square
		// region around the center of the map so that it can grow out uniformly
		for (int y = 0; y < mapHeight; y++)
		{
			for (int x = 0; x < mapWidth; x++)
			{
				for (int z = 0; z < numberOfInputs; z++)
				{
					//Set the random weight
					weights[x][y][z] = 0.5 + (0.2 * Math.random() - 0.1);
				}

				//Set all of the initial data labels to zero
				labels[x][y][0]  = 0.0;
			}
		}
	}

	//Train the map
	public void train(double[] new_inputs)
	{
		//Find the position in the map of our inputs
		findWinner(new_inputs);

		//UPdate the weights accordingly
		updateMap();

		//Now decrease the learning rate and radius a little
		currentLearningRate = minimumLearningRate + ((currentLearningRate-minimumLearningRate)*learningRateDecay);
		currentUpdateRadius = minimumUpdateRadius + ((currentUpdateRadius-minimumUpdateRadius)*updateRadiusDecay);
	}

	//Test the map by getting the last input (set by the demo class) and finding
	// its position in the wieghts array. Then return the label that matches the
	// position of the winner.
	public double test(double[] new_inputs)
	{
		findWinner(new_inputs);
		return labels[winnerXPos][winnerYPos][0];
	}


	//This function is about locating a position in the weights map based on
	// the input data. We step through every node in the weights array and check
	// its distance to the input data using the distanceBetweenPoints() method.
	//We keep a running 'best' value so that we know who won and we eventually
	// set the global variables winnerXPos and winnerYPos when we're done.
	public void findWinner(double[] new_inputs)
	{
		//We need to copy the array because java passes its arguments
		// as references (wasnt working that out a fun 2 hours wasted!)
		System.arraycopy(new_inputs, 0, inputs, 0, inputs.length);

		//We'll store the eventual winner in these global variables
		winnerXPos = 0;
		winnerYPos = 0;

		double bestResult = distanceBetweenPoints(weights[winnerXPos][winnerYPos], inputs);

		//Step through every node in our weights matrix to find the one
		// that is closest to our input. We use another method to do the calculation
		// we just need to send each point to it.
		for (int y = 0; y < mapHeight; y++)
		{
			for (int x = 0; x < mapWidth; x++)
			{
				if (distanceBetweenPoints(weights[x][y], inputs) < bestResult)
				{
					bestResult = distanceBetweenPoints(weights[x][y], inputs);
					winnerXPos = x;
					winnerYPos = y;
				}
			}
		}

	}//public void findWinner(double[] new_inputs)



	//This function is used to update the weights on the map. Before it is
	// called we need to have found a 'winning' node using the findWinner()
	// function. THe winning node is stored in winnerXPos and winnerYPos.
	public void updateMap()
	{
		//First we need to work how how far around the node we will update
		//We do this by getting the position of the winner then stepping to
		// the 'sides' of the node by the value of the updateRadius (which
		// starts as a value and decreases as we train so that hte map
		// stabalizes. We just round it so that we can step a whole number of
		// nodes to one side.
		int updateRadius = (int) Math.round(currentUpdateRadius);
		int x1 = winnerXPos - updateRadius;
		int y1 = winnerYPos - updateRadius;
		int x2 = winnerXPos + updateRadius;
		int y2 = winnerYPos + updateRadius;

		//Now are x1,x2,y1,y2 values hold the locations of points either
		// side ofthe winner (unless the update radius has reached < 1 of cours)

		//If our borders are not allowed to wrap around we need to make sure
		// that our x and y values are not larger than the sides of the map.
		//I borrowed some of this code. See code acknowledgements.
		if (!wrapAroundMapBorders)
		{
			x1 = Math.max(x1, 0);
			y1 = Math.max(y1, 0);
			x2 = Math.min(x2, mapWidth-1);
			y2 = Math.min(y2, mapHeight-1);
		}

		//WHen we start working out distances between nodes from the winning
		// node (to work out if they exist within the update radius) it will
		// help to have some temp variables (saves messy calculation code at
		// a lot of different points). Thats what these are for.
		int dx = 0;
		int dy = 0;
		double distance = 0.0;

		//When we're working on a particular node we'll store it here
		int nodeXpos = 0;
		int nodeYpos = 0;

		//We'll hold the existing weights in a temporary double array.
		double[] existingWeights = null;

		//Now step through each of the nodes that fall inside the X and Y
		// points we worked out above and adjust their weights.
		for (int y = y1; y <= y2; y++)
		{
			//Get the distance between this node and the winning node (we
			// use this down inside the X loop).
			dy = y - winnerYPos;
			// Normalise the node position (get rid of nasty negative values)
			nodeYpos = (y + mapHeight) % mapHeight;

			for (int x = x1; x <= x2; x++)
			{
				//Work out the distance between the current node and the winning
				// node (taking account of the map wrapping around)
				dx = Math.min(Math.abs(x - winnerXPos), Math.abs((x + mapWidth) - winnerXPos));

				// Normalise the node position (get rid of nasty negative values)
				nodeXpos = (x + mapWidth) % mapWidth;

				//Do the final square root
				distance = Math.sqrt(dx*dx + dy*dy);

				//Only update nodes inside the update radius
				if (distance < currentUpdateRadius)
				{
					//Take a reference to the real data and plug it into
					// our temporary array. As we update it the real data will
					// be updated (BECAUSE ITS A REFERENCE!)
					existingWeights = weights[nodeXpos][nodeYpos];

					//Adjust the nurons weights
					for (int i = 0; i < numberOfInputs; i++)
					{
						//Shift the existing weights closer to the input value
						// but as a factor of the currentLearningRate which will decrease
						// over time
						existingWeights[i] += currentLearningRate * (inputs[i] - existingWeights[i]);
					}

					//Set the nurons label. We have to make sure we set this
					// when we add new input data </SLOPPY CODE>
					this.labels[nodeXpos][nodeYpos][0] = inputLabel;

				}
			} //for (int x = x1; x <= x2; x++)
		}//for (int y = y1; y <= y2; y++)
	}//final private void updateMap()


	//This function is used to work out the 'Euclidean' distance between
	// two points. Hoever the points may exist in any number of dimentions
	// just so long as both have the same number of dimentions
	//I borrowed some of this code. See code acknowledgements.
	public double distanceBetweenPoints(double[] a, double[] b)
	{
		double diff;
		double sumOfDifferences = 0.0;
		double result = 0;
		for (int i = 0; i < a.length; i++)
		{
			diff = a[i]-b[i];
			sumOfDifferences += diff*diff;
		}

		//Work out the square root to get the actual distances
		result = Math.sqrt(sumOfDifferences);
		return result;
	}

	//Return the winning node
	//This will have been set last time we 'activated()' a node (normally
	// while training the map) this method just returns it in a nice neat
	// two element int array.
	public int[] getWinner()
	{
		int[] winner = new int[2];
		winner[0]   = winnerXPos;
		winner[1]   = winnerYPos;

		return winner;
	}

	//Return all of the guts of our wieghts array. So you can take a snapshot
	// of the map. Java passes by reference so theoretically we pass an array
	// to this to get populated.
	public double[][][] getWeights(double[][][] returnWeights)
	{
		for (int i = 0; i < weights.length; i++)
		{
			for (int j = 0; j < weights[i].length; j++)
			{
				System.arraycopy(weights[i][j], 0, returnWeights[i][j], 0, weights[i][j].length);
			}
		}

		return returnWeights;
	}

} //KohonenMap