/***************************************************************************************************
*                   C# sample for the usage of TopoART-AM (class Fast_TopoART_AM)                  *
****************************************************************************************************
*                            Created by Marko Tscherepanow, 9 March 2019                           *
***************************************************************************************************/

// Compile and run from the console: dotnet run --project TopoART-AM_sample1.csproj

using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using LibTopoART;
using System.Globalization;

namespace LibTopoART_samples
{
	/// <summary>
	/// Sample using TopoART-AM with synthetic two-dimensional data. [C#]
	/// <para>
	/// A TopoART-AM network is trained with the well-known Two Spirals dataset augmented with additional information.
	/// The resulting network maps two-dimensional points lying on each spiral (key 1) to their Euclidean distance from 
	/// the origin and the corresponding spiral ID (key 2). Therefore, it can recall spiral points if a distance and a
	/// spiral ID are given, and vice versa.
	/// </para>
	/// <para>The resulting network can be visualised using the script <c>ShowTopoARTAMResults</c> 
	/// provided for R and MATLAB in the subfolder <c>visualisation</c>.
	/// </para>
	/// </summary>
	class TopoART_AM_sample1
	{
		private static void Main()
		{
			// Dataset
			const string dataset				=	"../../../../../data/TwoSpirals_dataset.txt";
			// Destination directory for trained networks
			const string networkPath			=	"../../../../../results/networks/";

			const long sampleNumber				=	194;
			const long interpolatedSampleNumber	=	20000;

			// Dimensions of the keys
			const long key1Dimension = 2;
			const long key2Dimension = 2;

			var key1Array = new decimal[sampleNumber + interpolatedSampleNumber][];
			var key2Array = new decimal[sampleNumber + interpolatedSampleNumber][];

			// Set working directory to assembly directory
			Directory.SetCurrentDirectory(Path.GetDirectoryName(new Uri(Assembly.GetEntryAssembly().Location).LocalPath));

			// Load dataset and create keys (key 1: spiral points as two-dimensional coordinates; 
			// key 2: Euclidean distance from the origin and a numerical value reflecting the spiral ID)
			using(var datasetFile = new StreamReader(File.OpenRead(dataset))) {
				var rnd = new Random();

				// Use original Two Spirals dataset
				for(long i = 0; i < sampleNumber; ++i) {
					var numbers = Regex.Split(datasetFile.ReadLine(), @"\s+");

					key1Array[i] = new decimal[key1Dimension];
					// key1Array[i] consists of the original coordinates of the two spirals dataset normalised to the interval [0, 1.0]
					key1Array[i][0] = Decimal.Parse(numbers[1], NumberStyles.Float, CultureInfo.InvariantCulture);
					key1Array[i][1] = Decimal.Parse(numbers[2], NumberStyles.Float, CultureInfo.InvariantCulture);
					key1Array[i][0] = (key1Array[i][0] + 7.0m) / 14m;
					key1Array[i][1] = (key1Array[i][1] + 7.0m) / 14m;

					key2Array[i] = new decimal[key2Dimension];
					// key2Array[i][0] is computed as the Euclidean distance of key1Array[i] from the centre of its input space (i.e., from [0.5, 0.5])
					key2Array[i][0] = Math.Min((decimal)Math.Sqrt((double)((key1Array[i][0] - 0.5m) * (key1Array[i][0] - 0.5m) + (key1Array[i][1] - 0.5m) * (key1Array[i][1] - 0.5m))) * 2, 1.0m);
					// key2Array[i][1] is set to a random value in [0.0, 0.1] for the first spiral and set to a random value in [0.9, 1.0] for the second spiral
					decimal idOffset = (decimal)(rnd.NextDouble() * 0.1);
					key2Array[i][1] = numbers[3].Equals("SPIRAL1") ? 0.0m + idOffset : 1.0m - idOffset;
				}

				// Add additional interpolated data, as otherwise TopoART-AM (like TopoART) cannot link the categories to clusters 
				// (Remark: The modified learning mechanism of Episodic TopoART does not require these interpolated data.)
				for(long i = sampleNumber; i < sampleNumber + interpolatedSampleNumber;) {
					var sampleOffset = (decimal)(rnd.NextDouble() * (sampleNumber - 1));
					var startIndex = (long)sampleOffset;

					// Check maximum index and spiral ID
					if((startIndex < sampleNumber - 1) && (Math.Round(key2Array[startIndex][1]) == Math.Round(key2Array[startIndex + 1][1]))) {
						var fraction = sampleOffset - startIndex;

						key1Array[i] = new decimal[key1Dimension];
						// Fill key1Array[i] with additional interpolated values
						key1Array[i][0] = fraction * key1Array[startIndex][0] + (1 - fraction) * key1Array[startIndex + 1][0];
						key1Array[i][1] = fraction * key1Array[startIndex][1] + (1 - fraction) * key1Array[startIndex + 1][1];

						key2Array[i] = new decimal[key2Dimension];
						// key2Array[i][0] is computed as the Euclidean distance of key1Array[i] from the centre of its input space (i.e. from [0.5, 0.5])
						key2Array[i][0] = Math.Min((decimal)Math.Sqrt((double)((key1Array[i][0] - 0.5m) * (key1Array[i][0] - 0.5m) + (key1Array[i][1] - 0.5m) * (key1Array[i][1] - 0.5m))) * 2, 1.0m);
						// key2Array[i][1] is set to a random value in [0.0, 0.1] for the first spiral and set to a random value in [0.9, 1.0] for the second spiral
						decimal idOffset = (decimal)(rnd.NextDouble() * 0.1);
						key2Array[i][1] = (key2Array[startIndex][1] < 0.5m) ? 0.0m + idOffset : 1.0m - idOffset;

						++i;
					}
				}
			}

			// Initialise a TopoART-AM network with appropriate parameter values
			var ftam = new Fast_TopoART_AM(key1Dimension, key2Dimension, 2, 0.85m)
				{ Beta_sbm = 0.1m, Phi = 1, Tau = 200 };

			// Train network
			for(long i = 0; i < sampleNumber + interpolatedSampleNumber; ++i)
				ftam.Learn(key1Array[i], key2Array[i]);

			// Determine clusters
			ftam.ComputeClusterIDs();

			// Save network in human-readable form
			ftam.SaveText(networkPath + "Fast_TopoART-AM_TwoSpirals_dataset.txt");

			// Save network in binary form
			ftam.Save(networkPath + "Fast_TopoART-AM_TwoSpirals_dataset.ftam");

			// Recall key 1 for a given key 2
			var recallKey2Vec = new decimal[key2Dimension];
			recallKey2Vec[0] = 0.25m;	// distance from the origin
			recallKey2Vec[1] = 0.0m;	// spiral (0 to 0.1 = SPIRAL1, 0.9 to 1 = SPIRAL2)

			// Start recall process
			long recallClusterNum = ftam.BeginRecallKey1(recallKey2Vec);
			Console.WriteLine("Consider " + recallClusterNum + " clusters during recall");
			if(recallClusterNum != 2)
				// Such outcomes are possible due to the usage of random numbers. They do not constitute errors but
				// complicate the interpretation of the results shown below.
				Console.WriteLine("Warning: The expected cluster number differs from the actual one. Please repeat the training in case of any uncertainties.");

			// The first recall step recalls the best result for the distance and spiral specified by recallKey2Vec.
			if(ftam.RecallStep(out decimal[] recallResult, out decimal F3_activation))
				Console.WriteLine("1st recall step: Approximated key 1 = (" + recallResult[0] + ", " + recallResult[1] + ")^T, " + "F3_Activation = " + F3_activation);

			// The second recall step recalls the best result for the second cluster (i.e., the best fitting distance for the remaining spiral).
			// The reduced value of F3_activation reflects the dissimilarity caused by the association with the other spiral.
			if(ftam.RecallStep(out recallResult, out F3_activation))
				Console.WriteLine("2nd recall step: Approximated key 1 = (" + recallResult[0] + ", " + recallResult[1] + ")^T, " + "F3_Activation = " + F3_activation);

			// Finish recall process
			ftam.EndRecall();
		}
	}
}