
// geneticPrediction provides functions to train and query neural network models
// These models are used to predict attributes such as eye color and autism from person genome files

package geneticPrediction

// I am a novice in the ways of neural networks.
// Machine learning experts should chime in and offer improvements.
// We have to make sure that model inference remains very fast
// Sorting matches by offspring total polygenic disease score will require inference on dozens of models for each match

import "seekia/internal/genetics/geneticReferences/polygenicDiseases"
import "seekia/internal/genetics/geneticReferences/traits"
import "seekia/internal/genetics/geneticPredictionModels"
import "seekia/internal/genetics/locusValue"
import "seekia/internal/genetics/readBiobankData"
import "seekia/internal/helpers"

import "gorgonia.org/gorgonia"
import "gorgonia.org/tensor"

import goRng "github.com/leesper/go_rng"

import mathRand "math/rand/v2"
import "math"
import "bytes"
import "encoding/gob"
import "slices"
import "errors"

type PersonAttributeData struct{

	// Example: "Height", "Eye Color"
	AttributeName string

	// A map to store all of the person's locus values for this attribute
	// This map may contain locations which this person's genome doesn't actually
	//		possess a common-enough allele to use to create training data
	LocusValuesMap map[int64]locusValue.LocusValue

	AttributeIsNumeric bool

	// These variables store the attribute value for this person
	// Only 1 of the 2 will have any data
	// For example: Discete could be "Blue" for eye color or numeric could be 40 for age

	AttributeValue_Discrete string
	AttributeValue_Numeric float64
}


// This struct stores information about where a genome locus's information is stored in a neural network input tensor
type GenomeLocusTensorIndexesInfo struct{

	// The neuron index which tells you if the locus exists
	LocusExistsIndex int

	// The neuron index which tells you if the locus is phased
	LocusIsPhasedIndex int

	// Map Structure: Allele1 Value -> The neuron index which is used to represent that allele value
	// Example: "G" -> 1350, "C" -> 1351
	Allele1IndexesMap map[string]int

	// Map Structure: Allele2 Value -> The neuron index which is used to represent that allele value
	// Example: "G" -> 1350, "C" -> 1351
	Allele2IndexesMap map[string]int
}

// This object is used to convert a genome to a neural network input tensor
type AttributeGenomeTensorConversionGuide struct{

	// This represents the total quantity of neurons for the input layer
	QuantityOfInputNeurons int

	// Map Structure: The locus rsID -> GenomeLocusTensorIndexesInfo
	GenomeLocusTensorIndexesMap map[int64]GenomeLocusTensorIndexesInfo
}

// This struct stores a person's training data
// Each TrainingData represents a single data example
// For example, the InputLayer is a column of neurons representing a person's genetics,
//		and the OutputLayer is a column representing their phenotype, such as eye color
type TrainingData struct{

	// These variables store the quantity of loci known and phased from the total set of all loci,
	// 		not the subset of loci used for the neural network
	//TODO: Add QuantityOfLociKnown_NeuralNetwork & QuantityOfKnownLoci_Total?
	QuantityOfLociKnown int
	QuantityOfLociKnownAndPhased int

	// InputLayer stores relevant rsID values for each attribute from the person's genomes
	// It also stores if each rsID is phased and if each rsID exists
	InputLayer []float32

	// OutputLayer stores person phenotype data as neurons
	// Each neuron represents an outcome
	// For example, for Eye Color, each neuron represents an eye color
	OutputLayer []float32
}

func EncodePersonAttributeDataObjectToBytes(inputPersonAttributeData PersonAttributeData)([]byte, error){

	buffer := new(bytes.Buffer)

	encoder := gob.NewEncoder(buffer)

	err := encoder.Encode(inputPersonAttributeData)
	if (err != nil) { return nil, err }

	attributeDataBytes := buffer.Bytes()

	return attributeDataBytes, nil
}

func DecodeBytesToPersonAttributeDataObject(inputPersonAttributeData []byte)(PersonAttributeData, error){

	if (inputPersonAttributeData == nil){
		return PersonAttributeData{}, errors.New("DecodeBytesToPersonAttributeDataObject called with nil inputPersonAttributeData.")
	}

	buffer := bytes.NewBuffer(inputPersonAttributeData)

	decoder := gob.NewDecoder(buffer)

	var newPersonAttributeData PersonAttributeData

	err := decoder.Decode(&newPersonAttributeData)
	if (err != nil){ return PersonAttributeData{}, err }

	return newPersonAttributeData, nil
}



func EncodeAttributeGenomeTensorConversionGuideObjectToBytes(inputConversionGuide AttributeGenomeTensorConversionGuide)([]byte, error){

	buffer := new(bytes.Buffer)

	encoder := gob.NewEncoder(buffer)

	err := encoder.Encode(inputConversionGuide)
	if (err != nil) { return nil, err }

	conversionGuideBytes := buffer.Bytes()

	return conversionGuideBytes, nil
}

func DecodeBytesToAttributeGenomeTensorConversionGuideObject(inputConversionGuideBytes []byte)(AttributeGenomeTensorConversionGuide, error){

	if (inputConversionGuideBytes == nil){
		return AttributeGenomeTensorConversionGuide{}, errors.New("DecodeBytesToAttributeGenomeTensorConversionGuideObject called with nil inputConversionGuideBytes.")
	}

	buffer := bytes.NewBuffer(inputConversionGuideBytes)

	decoder := gob.NewDecoder(buffer)

	var newAttributeGenomeTensorConversionGuide AttributeGenomeTensorConversionGuide

	err := decoder.Decode(&newAttributeGenomeTensorConversionGuide)
	if (err != nil){ return AttributeGenomeTensorConversionGuide{}, err }

	return newAttributeGenomeTensorConversionGuide, nil
}


func EncodeTrainingDataObjectToBytes(inputTrainingData TrainingData)([]byte, error){

	buffer := new(bytes.Buffer)

	encoder := gob.NewEncoder(buffer)

	err := encoder.Encode(inputTrainingData)
	if (err != nil) { return nil, err }

	trainingDataBytes := buffer.Bytes()

	return trainingDataBytes, nil
}

func DecodeBytesToTrainingDataObject(inputTrainingData []byte)(TrainingData, error){

	if (inputTrainingData == nil){
		return TrainingData{}, errors.New("DecodeBytesToTrainingDataObject called with nil inputTrainingData.")
	}

	buffer := bytes.NewBuffer(inputTrainingData)

	decoder := gob.NewDecoder(buffer)

	var newTrainingData TrainingData

	err := decoder.Decode(&newTrainingData)
	if (err != nil){ return TrainingData{}, err }

	return newTrainingData, nil
}

//Inputs:
//	-AttributeGenomeTensorConversionGuide
//	-[]int64: Attribute rsIDs list (for determining quantity of Known/Phased loci)
//		-We can't use the AttributeGenomeTensorConversionGuide because it contains a subset of all of the trait loci
//		-The neural networks may not take every loci from each trait as input
//	-map[int64]locusValue.LocusValue
//Outputs:
//	-bool: Person's genome has at least 1 location with a common-enough allele used by the prediction model present
//	-[]float32: Input layer for neural network
//	-int: Quantity of known loci (For all loci, not just ones used by the prediction model)
//	-int: Quantity of phased loci (For all loci, not just ones used by the prediction model)
//	-error
func CreateInputNeuralNetworkLayerFromGenomeMap(attributeGenomeTensorConversionGuide AttributeGenomeTensorConversionGuide, allRsidsList []int64, genomeMap map[int64]locusValue.LocusValue)(bool, []float32, int, int, error){

	inputLayerLength := attributeGenomeTensorConversionGuide.QuantityOfInputNeurons
	genomeLocusTensorIndexesMap := attributeGenomeTensorConversionGuide.GenomeLocusTensorIndexesMap

	neuralNetworkInputLayer := make([]float32, inputLayerLength)

	anyNeuralNetworkLocusKnown := false

	for rsID, genomeLocusTensorIndexesInfo := range genomeLocusTensorIndexesMap{

		personLocusValue, exists := genomeMap[rsID]
		if (exists == false){
			continue
		}

		anyNeuralNetworkLocusKnown = true

		locusIsPhased := personLocusValue.LocusIsPhased
		locusAllele1Value := personLocusValue.Base1Value
		locusAllele2Value := personLocusValue.Base2Value

		// The neuron index which tells you if the locus exists
		locusExistsIndex := genomeLocusTensorIndexesInfo.LocusExistsIndex

		// The neuron index which tells you if the locus is phased
		locusIsPhasedIndex := genomeLocusTensorIndexesInfo.LocusIsPhasedIndex

		// Map Structure: Allele1 Value -> The neuron index which is used to represent that allele value
		// Example: "G" -> 1350, "C" -> 1351
		allele1IndexesMap := genomeLocusTensorIndexesInfo.Allele1IndexesMap

		// Map Structure: Allele2 Value -> The neuron index which is used to represent that allele value
		// Example: "G" -> 1350, "C" -> 1351
		allele2IndexesMap := genomeLocusTensorIndexesInfo.Allele2IndexesMap

		allele1Index, exists := allele1IndexesMap[locusAllele1Value]
		if (exists == false){
			// This happens if a person has an allele value was rare enough/nonexistent in the training data,
			//		so we didnt use it for training the neural network model
			continue
		}

		allele2Index, exists := allele2IndexesMap[locusAllele2Value]
		if (exists == false){
			// This happens if a person has an allele value was rare enough/nonexistent in the training data,
			//		so we didnt use it for training the neural network model
			continue
		}

		neuralNetworkInputLayer[locusExistsIndex] = 1

		if (locusIsPhased == true){
			neuralNetworkInputLayer[locusIsPhasedIndex] = 1
		}

		neuralNetworkInputLayer[allele1Index] = 1
		neuralNetworkInputLayer[allele2Index] = 1
	}

	if (anyNeuralNetworkLocusKnown == false){
		return false, nil, 0, 0, nil
	}

	// Now we count quantityOfKnownLoci and quantityOfPhasedLoci
	quantityOfLociKnown := 0
	quantityOfPhasedLoci := 0

	for _, rsID := range allRsidsList{

		personLocusValue, exists := genomeMap[rsID]
		if (exists == false){
			continue
		}

		quantityOfLociKnown += 1

		locusIsPhased := personLocusValue.LocusIsPhased
		if (locusIsPhased == true){
			quantityOfPhasedLoci += 1
		}
	}

	return true, neuralNetworkInputLayer, quantityOfLociKnown, quantityOfPhasedLoci, nil
}

// This function returns which outcome is being described from a neural network's final output layer
// This is only used for discrete traits
// Outputs:
//	-string: Output Name (Example: "Blue")
//	-error
func GetDiscreteOutcomeNameFromOutputLayer(traitName string, verifyOutputLayer bool, outputLayer []float32)(string, error){

	if (verifyOutputLayer == true){

		// We make sure all neurons sum to 1

		summedNeurons := float32(0)

		for _, neuronValue := range outputLayer{
			summedNeurons += neuronValue
		}

		// We allow a small amount of inaccuracy due to the imprecise nature of floats.
		if (summedNeurons > 1.01 || summedNeurons < .99){
			summedNeuronsString := helpers.ConvertFloat32ToString(summedNeurons)
			return "", errors.New("GetOutcomeNameFromOutputLayer called with layer containing neuron values which don't sum to 1: " + summedNeuronsString)
		}
	}

	getBiggestNeuronIndex := func()int{

		biggestNeuronValue := float32(0)
		biggestNeuronIndex := 0

		for index, neuronValue := range outputLayer{

			if (index == 0){
				biggestNeuronValue = neuronValue
			} else {

				if (neuronValue > biggestNeuronValue){
					biggestNeuronValue = neuronValue
					biggestNeuronIndex = index
				}
			}
		}

		return biggestNeuronIndex
	}

	biggestNeuronIndex := getBiggestNeuronIndex()

	switch traitName{

		case "Eye Color":{

			if (len(outputLayer) != 4){
				return "", errors.New("GetOutcomeNameFromOutputLayer called with invalid length output layer.")
			}

			switch biggestNeuronIndex{
				case 0:{
					return "Blue", nil
				}
				case 1:{
					return "Green", nil
				}
				case 2:{
					return "Hazel", nil
				}
				case 3:{
					return "Brown", nil
				}
			}
		}
		case "Lactose Tolerance":{

			if (len(outputLayer) != 2){
				return "", errors.New("GetOutcomeNameFromOutputLayer called with invalid length output layer.")
			}

			switch biggestNeuronIndex{
				case 0:{
					return "Tolerant", nil
				}
				case 1:{
					return "Intolerant", nil
				}
			}
		}
	}

	return "", errors.New("GetOutcomeNameFromOutputLayer called with unknown traitName: " + traitName)
}


// This function returns which outcome is being described from a neural network's final output layer
// This is only used for numeric traits and polygenic diseases
// Outputs:
//	-float64: Output Value (example: 150 centimeters)
//	-error
func GetNumericOutcomeValueFromOutputLayer(attributeName string, outputLayer []float32)(float64, error){

	if (len(outputLayer) != 1){
		return 0, errors.New("GetNumericOutcomeValueFromOutputLayer called with output layer which is not length of 1")
	}

	outputNeuron := outputLayer[0]

	if (outputNeuron < 0 || outputNeuron > 1){
		return 0, errors.New("GetNumericOutcomeValueFromOutputLayer called with output layer contains out-of-bounds neuron")
	}

	getOutcomeMinAndMax := func()(float64, float64, error){

		switch attributeName{
			case "Height":{
				// Shortest person of all time: 54 cm
				// Tallest person of all time: 272 cm
				return 54, 272, nil
			}
			case "Autism",
				"Homosexualness",
				"Obesity":{
				return 0, 10, nil
			}
		}

		return 0, 0, errors.New("GetNumericOutcomeValueFromOutputLayer called with unknown attributeName: " + attributeName)
	}

	outcomeMin, outcomeMax, err := getOutcomeMinAndMax()
	if (err != nil) { return 0, err }

	outcomeValue, err := helpers.ScaleFloat64Proportionally(true, float64(outputNeuron), 0, 1, outcomeMin, outcomeMax)
	if (err != nil) { return 0, err }

	return outcomeValue, nil
}

//Outputs:
//	-int: Layer 1 neuron count (input layer)
//	-int: Layer 2 neuron count
//	-int: Layer 3 neuron count
//	-int: Layer 4 neuron count (output layer)
//	-error
func getNeuralNetworkLayerSizes(attributeName string)(int, int, int, int, error){

	switch attributeName{

		case "Eye Color":{

			// There are 4 output neurons, each representing a color
			// There are 4 colors: Blue, Green, Brown, Hazel

			return 529, 200, 150, 4, nil
		}
		case "Lactose Tolerance":{

			// There are 2 output neurons, each representing a tolerance: Tolerant, Intolerant
			return 13, 11, 7, 2, nil
		}
		case "Height":{

			// There is 1 output neuron, representing a height value
			return 145, 75, 15, 1, nil
		}
		case "Autism":{

			// There is 1 output neuron, representing an autism value
			return 1033, 100, 50, 1, nil
		}
		case "Homosexualness":{

			// There is 1 output neuron, representing a homosexualness value
			return 7, 5, 3, 1, nil
		}
		case "Obesity":{

			// There is 1 output neuron, representing an obesity value
			return 25, 15, 5, 1, nil
		}
	}

	return 0, 0, 0, 0, errors.New("getNeuralNetworkLayerSizes called with unknown attributeName: " + attributeName)
}


//Outputs:
//	-[]int64: All Attribute rsIDs list
//	-error
func GetAttributeLociList(attributeName string)([]int64, error){

	switch attributeName{

		case "Eye Color",
			"Lactose Tolerance",
			"Height",
			"Homosexualness":{

			traitObject, err := traits.GetTraitObject(attributeName)
			if (err != nil) { return nil, err }

			// This is a list of rsIDs which influence this trait
			traitLociList := traitObject.LociList

			return traitLociList, nil
		}
		case "Autism",
			"Obesity":{

			diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(attributeName)
			if (err != nil) { return nil, err }

			// This is a list of rsIDs which influence this disease
			diseaseLociList := diseaseObject.LociList

			return diseaseLociList, nil
		}
	}

	return nil, errors.New("GetAttributeLociList called with unknown attributeName: " + attributeName)
}

// This function returns the attribute value for a person
//
// Outputs:
//	-bool: Person attribute value is known
//	-bool: Attribute is numeric
//	-string: Person Attribute Value (Discrete)
//	-float64: Person Attribute Value (Numeric)
//	-error
func GetPersonAttributeValueFromPhenotypeDataObject(attributeName string, personPhenotypeDataObject readBiobankData.PhenotypeData_OpenSNP)(bool, bool, string, float64, error){

	switch attributeName{

		case "Eye Color":{

			personEyeColorIsKnown := personPhenotypeDataObject.EyeColorIsKnown
			if (personEyeColorIsKnown == false){
				return false, false, "", 0, nil
			}

			personEyeColor := personPhenotypeDataObject.EyeColor

			return true, false, personEyeColor, 0, nil
		}
		case "Lactose Tolerance":{

			personLactoseToleranceIsKnown := personPhenotypeDataObject.LactoseToleranceIsKnown
			if (personLactoseToleranceIsKnown == false){
				return false, false, "", 0, nil
			}

			personLactoseTolerance := personPhenotypeDataObject.LactoseTolerance

			personLactoseToleranceString := helpers.ConvertBoolToYesOrNoString(personLactoseTolerance)

			return true, false, personLactoseToleranceString, 0, nil
		}
		case "Height":{

			personHeightIsKnown := personPhenotypeDataObject.HeightIsKnown
			if (personHeightIsKnown == false){
				return false, false, "", 0, nil
			}

			personHeight := personPhenotypeDataObject.Height

			return true, true, "", personHeight, nil
		}
		case "Autism":{

			personAutismIsKnown := personPhenotypeDataObject.AutismIsKnown
			if (personAutismIsKnown == false){
				return false, false, "", 0, nil
			}

			personAutism := personPhenotypeDataObject.Autism

			return true, true, "", personAutism, nil
		}
		case "Homosexualness":{

			personHomosexualnessIsKnown := personPhenotypeDataObject.HomosexualnessIsKnown
			if (personHomosexualnessIsKnown == false){
				return false, false, "", 0, nil
			}

			personHomosexualness := personPhenotypeDataObject.Homosexualness

			return true, true, "", personHomosexualness, nil
		}
		case "Obesity":{

			personObesityIsKnown := personPhenotypeDataObject.ObesityIsKnown
			if (personObesityIsKnown == false){
				return false, false, "", 0, nil
			}

			personObesity := personPhenotypeDataObject.Obesity

			return true, true, "", personObesity, nil
		}
	}

	return false, false, "", 0, errors.New("GetPersonAttributeValue called with unknown attributeName: " + attributeName)
}


// This function returns training data to use to train each neural network prediction model
// Outputs:
//	-bool: Any training data created for provided person
//	-[]TrainingData: List of TrainingData for the person which we will use to train the model
//	-error
func CreatePersonGeneticPredictionTrainingData(personIndex int, personAttributeData PersonAttributeData, attributeGenomeTensorConversionGuide AttributeGenomeTensorConversionGuide)(bool, []TrainingData, error){

	attributeName := personAttributeData.AttributeName

	quantityOfInputNeurons := attributeGenomeTensorConversionGuide.QuantityOfInputNeurons
	genomeLocusTensorIndexesMap := attributeGenomeTensorConversionGuide.GenomeLocusTensorIndexesMap

	// Each layer is represented as a []float32
	// Each float is a value between 0 and 1
	//
	// Each TrainingData holds a variation of the person's genome rsID values
	// We add many rows with withheld data to improve training data

	numberOfInputLayerRows, _, _, numberOfOutputLayerRows, err := getNeuralNetworkLayerSizes(attributeName)
	if (err != nil) { return false, nil, err }

	if (numberOfInputLayerRows != quantityOfInputNeurons){

		expectedNumberOfInputLayerRowsString := helpers.ConvertIntToString(quantityOfInputNeurons)

		return false, nil, errors.New(attributeName + " numberOfInputLayerRows is not expected: " + expectedNumberOfInputLayerRowsString)
	}

	// This function returns the outputLayer for all trainingDatas for this person
	// Each outputLayer represents the person's attribute value (Example: "Blue" for Eye Color)
	// Each outputLayer is identical, because each TrainingData example belongs to the same person
	//
	// Outputs:
	//	-[]float32: Neuron values for layer
	//	-error
	getPersonAttributeValueNeurons := func()([]float32, error){

		//attributeIsNumeric := personAttributeData.AttributeIsNumeric

		attributeValue_Discrete := personAttributeData.AttributeValue_Discrete
		attributeValue_Numeric := personAttributeData.AttributeValue_Numeric

		switch attributeName{

			case "Eye Color":{

				personEyeColor := attributeValue_Discrete

				if (personEyeColor == "Blue"){

					return []float32{1, 0, 0, 0}, nil

				} else if (personEyeColor == "Green"){

					return []float32{0, 1, 0, 0}, nil

				} else if (personEyeColor == "Hazel"){

					return []float32{0, 0, 1, 0}, nil

				} else if (personEyeColor == "Brown"){

					return []float32{0, 0, 0, 1}, nil
				}

				return nil, errors.New("Malformed personAttributeData: Invalid eyeColor: " + personEyeColor)
			}
			case "Lactose Tolerance":{

				personLactoseTolerance := attributeValue_Discrete

				if (personLactoseTolerance == "Yes"){

					return []float32{1, 0}, nil

				} else if (personLactoseTolerance == "No"){

					return []float32{0, 1}, nil
				}

				return nil, errors.New("Malformed personAttributeData: Invalid LactoseTolerance: " + personLactoseTolerance)
			}
			case "Height":{

				personHeight := attributeValue_Numeric

				// Shortest person of all time: 54 cm
				// Tallest person of all time: 272 cm

				outputValue, err := helpers.ScaleFloat64Proportionally(true, personHeight, 54, 272, 0, 1)
				if (err != nil) { return nil, err }

				outputValueFloat32 := float32(outputValue)

				outputLayer := []float32{outputValueFloat32}

				return outputLayer, nil
			}
			case "Autism",
				"Homosexualness",
				"Obesity":{

				// All of these have a value between 0 and 1

				if (attributeValue_Numeric < 0 || attributeValue_Numeric > 1){

					attributeValueString := helpers.ConvertFloat64ToString(attributeValue_Numeric)

					return nil, errors.New("Malformed personAttributeData: Invalid " + attributeName + ": " + attributeValueString)
				}

				outputValueFloat32 := float32(attributeValue_Numeric)

				outputLayer := []float32{outputValueFloat32}

				return outputLayer, nil
			}
		}

		return nil, errors.New("Malformed personAttributeData: Unknown attributeName: " + attributeName)
	}

	personAttributeValueNeurons, err := getPersonAttributeValueNeurons()
	if (err != nil) { return false, nil, err }

	if (len(personAttributeValueNeurons) != numberOfOutputLayerRows){
		return false, nil, errors.New("getPersonAttributeValueNeurons returning invalid length layer slice.")
	}

	// This is a list of every single RSID used as input to the neural network
	// The person may or may not have information about each rsID
	allNeuralNetworkRSIDs := helpers.GetListOfMapKeys(genomeLocusTensorIndexesMap)

	// We sort it so our results are reproducable
	slices.Sort(allNeuralNetworkRSIDs)

	personLocusValuesMap := personAttributeData.LocusValuesMap

	// We first calculate the person's quantityOfLociKnown and quantityOfLociKnownAndPhased
	// We do this for all of the attribute's loci, rather than just the loci known for the neural network
	// This is because the neural network used to use every locus, rather than only loci that are common enough in the training set
	//TODO: Have 4 counts in the code instead of 2: quantityOfLociKnown_Total and quantityOfLociKnown_NeuralNetwork, + the phased counts

	personQuantityOfLociKnown := len(personLocusValuesMap)
	personQuantityOfLociKnownAndPhased := 0

	for _, locusValue := range personLocusValuesMap{

		locusIsPhased := locusValue.LocusIsPhased
		if (locusIsPhased == true){
			personQuantityOfLociKnownAndPhased += 1
		}
	}

	// We want the initial training data to be the same for each call of this function that has the same input parameters
	// This is a necessary step so our neural network models will be reproducable
	// Reproducable means that other people can run the code and produce the same models, byte-for-byte
	// We seed the random generator with the personIndex so that the probabilities/included loci
	//		 are not identical for each person until a difference between their alleles occurs

	pseudorandomNumberGenerator := mathRand.New(mathRand.NewPCG(123, uint64(personIndex)))

	// We create <=300 examples per person.
	// We will omit examples that are duplicates, which sometimes randomly occur
	// We randomize allele order whenever phase for the locus is unknown
	// 50% of the time we randomize allele order even when phase is known to train the model on unphased data
	// Unphased data is data where the order each allele (Example: G;A) has no meaning, because phase data was not captured
	// We randomize allele order to simulate unphased data
	// For example, if a person's genome is phased, we will randomize the base pair order and set the LocusIsPhased neuron to false
	// 
	// We also omit loci randomly. We cycle through 10-100% inclusion probability for each known locus.

	// We now add this person's data to the trainingDataList

	trainingDataList := make([]TrainingData, 0)

	for i:=0; i < 300; i++{

		// We use these variables to determine the QuantityOfLociKnown and QuantityOfLociKnownAndPhased for the training data
		// We need to do it this way because we aren't counting the quantities of loci in the inputLayer, but rather
		//		the quantities of loci known in total for all of the attribute loci
		// Many of the attribute's loci are not used in the neural network due to a lack of examples in the dataset training data
		// Thus, these quantities will almost always be greater than the quantities used in the inputLayer
		trainingDataQuantityOfLociKnown := personQuantityOfLociKnown
		trainingDataQuantityOfLociKnownAndPhased := personQuantityOfLociKnownAndPhased

		getRandomizePhaseBool := func()bool{

			if (i%2 == 0){
				return true
			}

			return false
		}

		randomizePhaseBool := getRandomizePhaseBool()

		intBetween0and9 := i%9
		intBetween1and10 := intBetween0and9 + 1

		probabilityOfUsingLoci := float64(intBetween1and10)/10

		// In the newInputLayer, each locus value is represented by a variable quantity of neurons:
		// Each neuron is represented by (0==false, 1==true)
		// Each locus's place in the tensor is as follows:
		//
		// Neuron 1: Locus value exists
		// Neuron 2: Locus value is phased
		// Neuron 3: Allele1 First Possible Value Is Present
		// Neuron 4: Allele1 Second Possible Value Is Present
		//	... For as many possible alleles that exist for Allele1
		// Repeat neurons 3-X for Allele2

		newInputLayer := make([]float32, quantityOfInputNeurons)

		anyLocusExists := false

		for _, rsID := range allNeuralNetworkRSIDs{

			personLocusValue, exists := personLocusValuesMap[rsID]
			if (exists == false){
				// This person's locus value is unknown
				continue
			}

			locusIsPhased := personLocusValue.LocusIsPhased

			if (probabilityOfUsingLoci != 1){

				randomFloat := pseudorandomNumberGenerator.Float64()
				if (randomFloat > probabilityOfUsingLoci){
					// This if statement has a (1-probabilityOfUsingLoci) chance of being true.
					// We are skipping this locus

					trainingDataQuantityOfLociKnown -= 1

					if (locusIsPhased == true){
						trainingDataQuantityOfLociKnownAndPhased -= 1
					}

					continue
				}
			}

			//Outputs:
			//	-bool: Locus is phased
			//	-string: Allele 1 value
			//	-string: Allele 2 value
			getNewLocusIsPhasedBoolAndLocusAlleles := func()(bool, string, string){

				locusAllele1 := personLocusValue.Base1Value
				locusAllele2 := personLocusValue.Base2Value

				if (locusAllele1 == locusAllele2){
					// Locus phase is unimportant
					return true, locusAllele1, locusAllele2
				}

				locusIsPhased := personLocusValue.LocusIsPhased

				if (randomizePhaseBool == false && locusIsPhased == true){
					return true, locusAllele1, locusAllele2
				}

				// We randomize the phase of the locus
				// We always do this if the locus is not phased, because the genome data might actually be partially/fully phased,
				//		even if it is not advertised as being phased

				randomNumber := pseudorandomNumberGenerator.IntN(2)
				if (randomNumber == 1){
					// This has a 50% chance of being true.
					return false, locusAllele1, locusAllele2
				}

				return false, locusAllele2, locusAllele1
			}

			newLocusIsPhased, locusAllele1, locusAllele2 := getNewLocusIsPhasedBoolAndLocusAlleles()

			if (locusIsPhased == true && newLocusIsPhased == false){
				// We converted a phased locus into an unphased one
				trainingDataQuantityOfLociKnownAndPhased -= 1
			}

			genomeLocusTensorIndexesInfo, exists := genomeLocusTensorIndexesMap[rsID]
			if (exists == false){
				return false, nil, errors.New("genomeLocusTensorIndexesMap missing rsID.")
			}

			// The neuron index which tells you if the locus exists
			locusExistsIndex := genomeLocusTensorIndexesInfo.LocusExistsIndex

			// The neuron index which tells you if the locus is phased
			locusIsPhasedIndex := genomeLocusTensorIndexesInfo.LocusIsPhasedIndex

			// Map Structure: Allele1 Value -> The neuron index which is used to represent that allele value
			// Example: "G" -> 1350, "C" -> 1351
			allele1IndexesMap := genomeLocusTensorIndexesInfo.Allele1IndexesMap

			// Map Structure: Allele2 Value -> The neuron index which is used to represent that allele value
			// Example: "G" -> 1350, "C" -> 1351
			allele2IndexesMap := genomeLocusTensorIndexesInfo.Allele2IndexesMap

			allele1Index, exists := allele1IndexesMap[locusAllele1]
			if (exists == false){
				// This happens if the user's allele value was too rare to be used in the training data
				continue
			}

			allele2Index, exists := allele2IndexesMap[locusAllele2]
			if (exists == false){
				// This happens if the user's allele value was too rare to be used in the training data
				continue
			}

			anyLocusExists = true

			// Now we add information to our layer of neurons

			newInputLayer[locusExistsIndex] = 1

			if (newLocusIsPhased == true){
				newInputLayer[locusIsPhasedIndex] = 1
			}

			newInputLayer[allele1Index] = 1
			newInputLayer[allele2Index] = 1
		}

		if (anyLocusExists == false){
			// We have 0 known loci for this training example.
			// We won't add it to the training data.
			// This happens sometimes if, when randomly excluding loci, we exclude all of the loci
			continue
		}

		checkForDuplicate := func()bool{

			// Now we check to make sure this is not a duplicate

			for _, trainingDataObject := range trainingDataList{

				existingInputLayer := trainingDataObject.InputLayer

				areEqual := slices.Equal(newInputLayer, existingInputLayer)
				if (areEqual == true){
					return true
				}
			}

			return false
		}

		duplicateExists := checkForDuplicate()
		if (duplicateExists == true){
			// We won't add this example to the training data.
			continue
		}

		personAttributeValueNeuronsCopy := slices.Clone(personAttributeValueNeurons)

		newTrainingData := TrainingData{
			QuantityOfLociKnown: trainingDataQuantityOfLociKnown,
			QuantityOfLociKnownAndPhased: trainingDataQuantityOfLociKnownAndPhased,
			InputLayer: newInputLayer,
			OutputLayer: personAttributeValueNeuronsCopy,
		}

		trainingDataList = append(trainingDataList, newTrainingData)
	}

	if (len(trainingDataList) == 0){
		// This can happen if the user doesn't possess any loci which were common enough in the
		//		training data to be used in the neural network
		return false, nil, nil
	}

	return true, trainingDataList, nil
}

func GetNewUntrainedNeuralNetworkObject(attributeName string)(*geneticPredictionModels.NeuralNetwork, error){

	layer1NeuronCount, layer2NeuronCount, layer3NeuronCount, layer4NeuronCount, err := getNeuralNetworkLayerSizes(attributeName)
	if (err != nil) { return nil, err }

	// This is the graph object we add each layer to
	newGraph := gorgonia.NewGraph()

	// We want the initial weights to be the same for each call of this function that has the same input parameters
	// This is a necessary step so our neural network models will be reproducable
	// Reproducable means that other people can run the code and produce the same models, byte-for-byte

	pseudorandomNumberGenerator := mathRand.New(mathRand.NewPCG(1, 2))

	// A layer is a column of neurons
	// Each neuron has an initial value between 0 and 1
	getNewNeuralNetworkLayerWeights := func(layerName string, layerNeuronRows int, layerNeuronColumns int)*gorgonia.Node{

		layerNameObject := gorgonia.WithName(layerName)

		layerShape := gorgonia.WithShape(layerNeuronRows, layerNeuronColumns)

		// We initialize the weights using He initialization
		gain := math.Sqrt(2.0)
		seed := pseudorandomNumberGenerator.Int64N(100000)
		layerInitObject := gorgonia.WithInit(heNormal(gain, seed))

		layerObject := gorgonia.NewMatrix(newGraph, tensor.Float32, layerShape, layerNameObject, layerInitObject)

		return layerObject
	}

	layer1 := getNewNeuralNetworkLayerWeights("Weights1", layer1NeuronCount, layer2NeuronCount)
	layer2 := getNewNeuralNetworkLayerWeights("Weights2", layer2NeuronCount, layer3NeuronCount)
	layer3 := getNewNeuralNetworkLayerWeights("Weights3", layer3NeuronCount, layer4NeuronCount)

	newNeuralNetworkObject := geneticPredictionModels.NeuralNetwork{

		Graph: newGraph,

		Weights1: layer1,
		Weights2: layer2,
		Weights3: layer3,
	}

	return &newNeuralNetworkObject, nil
}

// The below 2 functions are taken from gorgonia's weights.go
// We have to modify them to allow for a deterministic weight initialization

func heNormal(gain float64, seed int64) gorgonia.InitWFn {
	f := func(dt tensor.Dtype, s ...int) interface{} {
		switch dt {
		case tensor.Float32:
			return heEtAlN32(gain, seed, s...)
		default:
			panic("HeNormal only implemented for tensor.Float32.")
		}
	}
	return f
}


// heEtAlN32 returns float32 weights sampled from a normal distro, using the methods
// described in He et al (2015). The formula is:
//		randn(n) * sqrt(2/n)
// See also https://arxiv.org/abs/1502.01852
//
// For best results, use:
// 		1.0 for gain for weights that will be used in linear and/or sigmoid units
//		math.Sqrt(2.0) for gain for weights that will be used in ReLU units
//		math.Sqrt(2.0 / (1+alpha*alpha)) for ReLU that are leaky with alpha
func heEtAlN32(gain float64, seed int64, s ...int) []float32 {
	var fanIn float32

	switch len(s) {
	case 0, 1:
		panic("He et al only works with Tensors of dimensions >= 2")
	case 2:
		fanIn = float32(s[0])
	default:
		fanIn = 1.0
		for _, v := range s[1:] {
			fanIn *= float32(v)
		}
	}

	size := tensor.Shape(s).TotalSize()
	stdev := gain * math.Sqrt(float64(1.0/fanIn))

	rand := goRng.NewGaussianGenerator(seed)
	retVal := make([]float32, size)
	for i := range retVal {
		retVal[i] = float32(rand.Gaussian(0.0, stdev))
	}
	return retVal
}



// This function will train the neural network
// The function is passed a batch of TrainingData examples to train on
// Inputs:
//	-string: Attribute Name
//	-bool: Attribute is Numeric
//		-An example of a numeric attribute is Height
//		-An example of a discrete attribute is Eye Color, which has discrete outcomes (colors)
//	-*NeuralNetwork
//	-func()(bool, bool, TrainingData, error): Function to get the next training data.
//		-Outputs:
//		-bool: User stopped the training run
//		-bool: Another training data exists
//		-TrainingData: The next training data example
//		-error
// Outputs:
//	-bool: Process completed (was not stopped mid-way)
//	-[]float32: Slice of costs (Should decrease throughout the training process)
//	-error
func TrainNeuralNetwork(attributeName string, attributeIsNumeric bool, neuralNetworkObject *geneticPredictionModels.NeuralNetwork, getNextTrainingData func()(bool, bool, TrainingData, error))(bool, []float32, error){

	layer1NeuronCount, _, _, layer4NeuronCount, err := getNeuralNetworkLayerSizes(attributeName)
	if (err != nil) { return false, nil, err }

	neuralNetworkGraph := neuralNetworkObject.Graph

	// We first create the input and output nodes
	// They don't have any values yet.

	trainingDataInputNode := gorgonia.NewMatrix(neuralNetworkGraph,
		tensor.Float32,
		gorgonia.WithName("Input"),
		gorgonia.WithShape(1, layer1NeuronCount),
	)

	trainingDataExpectedOutputNode := gorgonia.NewMatrix(neuralNetworkGraph,
		tensor.Float32,
		gorgonia.WithName("ExpectedOutput"),
		gorgonia.WithShape(1, layer4NeuronCount),
	)

	//Outputs:
	//	-gorgonia.Solver
	//	-float64: Dropout probability for hidden layer 1
	//	-float64: Dropout probability for hidden layer 2
	//	-error
	getDropoutProbabilitesAndSolver := func()(gorgonia.Solver, float64, float64, error){

		switch attributeName{

			case "Eye Color":{

				learningRate := gorgonia.WithLearnRate(.001)
//				decayParameter := gorgonia.WithRho(0.9)
//			 	gradientClip := gorgonia.WithClip(5.0)

//				solver := gorgonia.NewRMSPropSolver(learningRate, decayParameter, gradientClip)
				solver := gorgonia.NewRMSPropSolver(learningRate)

				return solver, 0, 0, nil
			}
			case "Height",
				"Obesity",
				"Autism":{

				learningRate := gorgonia.WithLearnRate(.001)
//				decayParameter := gorgonia.WithRho(0.9)
//			 	gradientClip := gorgonia.WithClip(1.0)

//				solver := gorgonia.NewAdamSolver(learningRate, decayParameter, gradientClip)
//				solver := gorgonia.NewAdamSolver(learningRate, gradientClip)
				solver := gorgonia.NewRMSPropSolver(learningRate)

				return solver, 0.1, 0.1, nil
			}
			case "Lactose Tolerance",
				"Homosexualness":{

				learningRate := gorgonia.WithLearnRate(.0001)
				decayParameter := gorgonia.WithRho(0.99)
			 	gradientClip := gorgonia.WithClip(1.0)

				solver := gorgonia.NewAdamSolver(learningRate, decayParameter, gradientClip)

				return solver, 0.6, 0.5, nil
			}
		}

		return nil, 0, 0, errors.New("TrainNeuralNetwork called with unknown attributeName: " + attributeName)
	}

	solver, layer1DropoutProbability, layer2DropoutProbability, err := getDropoutProbabilitesAndSolver()
	if (err != nil) { return false, nil, err }

	err = neuralNetworkObject.BuildNeuralNetwork(trainingDataInputNode, attributeIsNumeric, true, layer1DropoutProbability, layer2DropoutProbability)
	if (err != nil) { return false, nil, err }

	// This computes the loss (how accurate was our prediction)
	losses, err := gorgonia.Sub(trainingDataExpectedOutputNode, neuralNetworkObject.Prediction)
	if (err != nil) { return false, nil, err }

	squareOfLosses, err := gorgonia.Square(losses)
	if (err != nil) { return false, nil, err }

	// Cost is an average of the square of losses
	cost, err := gorgonia.Mean(squareOfLosses)
	if (err != nil) { return false, nil, err }

	neuralNetworkLearnables := neuralNetworkObject.GetLearnables()

	// Grad takes a scalar cost node and a list of with-regards-to, and returns the gradient
	_, err = gorgonia.Grad(cost, neuralNetworkLearnables...)
	if (err != nil) { return false, nil, err }

	bindDualValues := gorgonia.BindDualValues(neuralNetworkLearnables...)

	// NewTapeMachine creates a Virtual Machine that compiles a graph into a prog.
	virtualMachine := gorgonia.NewTapeMachine(neuralNetworkGraph, bindDualValues)

	defer virtualMachine.Close()

	// We keep track of every cost for every prediction
	// We use this to track if it is decreasing during training
	costsList := make([]float32, 0)

	for {

		userStoppedTraining, nextDataExists, trainingDataObject, err := getNextTrainingData()
		if (err != nil) { return false, nil, err }
		if (userStoppedTraining == true){
			// User manually stopped the training run
			return false, nil, nil
		}
		if (nextDataExists == false){
			// We are done training
			break
		}

		// We convert our input training data slices to the type *Dense and assign them to our nodes

		// This inputLayer contains the allele values for this training example
		trainingDataInputLayer := trainingDataObject.InputLayer

		// This outputLayer contains the phenotype for this training example (example: Eye color of Blue)
		trainingDataOutputLayer := trainingDataObject.OutputLayer

		inputTensorShapeObject := tensor.WithShape(1, layer1NeuronCount)
		outputTensorShapeObject := tensor.WithShape(1, layer4NeuronCount)

		inputTensorBacking := tensor.WithBacking(trainingDataInputLayer)
		outputTensorBacking := tensor.WithBacking(trainingDataOutputLayer)

		inputTensor := tensor.New(inputTensorShapeObject, inputTensorBacking)
		outputTensor := tensor.New(outputTensorShapeObject, outputTensorBacking)

		err = gorgonia.Let(trainingDataInputNode, inputTensor)
		if (err != nil) { return false, nil, err }

		err = gorgonia.Let(trainingDataExpectedOutputNode, outputTensor)
		if (err != nil) { return false, nil, err }

		err = virtualMachine.RunAll()
		if (err != nil) { return false, nil, err }

		costValue := cost.Value().Data().(float32)
		costsList = append(costsList, costValue)

		// NodesToValueGrads is a utility function that converts a Nodes to a slice of ValueGrad for the solver
		valueGrads := gorgonia.NodesToValueGrads(neuralNetworkLearnables)

		err = solver.Step(valueGrads)
		if (err != nil) { return false, nil, err }

		virtualMachine.Reset()
	}

	return true, costsList, nil
}


// This function computes a raw prediction from the neural network
// Outputs:
//	-[]float32: Output neurons
//	-error
func GetNeuralNetworkRawPrediction(inputNeuralNetwork *geneticPredictionModels.NeuralNetwork, attributeIsNumeric bool, inputLayer []float32)([]float32, error){

	neuralNetworkGraph := inputNeuralNetwork.Graph

	numberOfInputNeurons := len(inputLayer)

	inputNode := gorgonia.NewMatrix(neuralNetworkGraph,
		tensor.Float32,
		gorgonia.WithName("Input"),
		gorgonia.WithShape(1, numberOfInputNeurons),
	)

	// We convert the inputLayer []float32 to a tensor *Dense object

	inputTensorShapeObject := tensor.WithShape(1, numberOfInputNeurons)

	inputTensorBacking := tensor.WithBacking(inputLayer)

	inputTensor := tensor.New(inputTensorShapeObject, inputTensorBacking)

	err := gorgonia.Let(inputNode, inputTensor)
	if (err != nil) { return nil, err }


	err = inputNeuralNetwork.BuildNeuralNetwork(inputNode, attributeIsNumeric, false, 0, 0)
	if (err != nil){ return nil, err }

	// Now we create a virtual machine to compute the prediction

	neuralNetworkLearnables := inputNeuralNetwork.GetLearnables()

	bindDualValues := gorgonia.BindDualValues(neuralNetworkLearnables...)

	virtualMachine := gorgonia.NewTapeMachine(neuralNetworkGraph, bindDualValues)

	err = virtualMachine.RunAll()
	if (err != nil) { return nil, err }

	prediction := inputNeuralNetwork.Prediction

	predictionValues := prediction.Value().Data().([]float32)

	return predictionValues, nil
}



