// trainedPredictionModels contains trained prediction neural network models for predicting genetic traits
// These models are stored as .gob encoded files of []float32 weights
// This package also contains prediction accuracy information for each model
// Prediction accuracy models describe information about how accurate the predictions made by the models are
// All of the files in this package are created by the Create Genetic Models utility.
// This utility is located in /utilities/createGeneticModels/createGeneticModels.go

package trainedPredictionModels

import "seekia/internal/genetics/geneticPrediction"
import "seekia/internal/genetics/geneticPredictionModels"
import "seekia/internal/genetics/locusValue"
import "seekia/internal/helpers"

import _ "embed"

import "math"
import "bytes"
import "encoding/gob"
import "slices"
import "sync"
import "errors"


// These are the trained prediction model files:

//go:embed predictionModels/EyeColorModel.gob
var predictionModelFile_EyeColor []byte

//go:embed predictionModels/LactoseToleranceModel.gob
var predictionModelFile_LactoseTolerance []byte

//go:embed predictionModels/HeightModel.gob
var predictionModelFile_Height []byte

//go:embed predictionModels/AutismModel.gob
var predictionModelFile_Autism []byte

//go:embed predictionModels/HomosexualnessModel.gob
var predictionModelFile_Homosexualness []byte

//go:embed predictionModels/ObesityModel.gob
var predictionModelFile_Obesity []byte

// These are the trained prediction models
// Each model has a mutex so it will only be used to make 1 prediction at a time

var predictionModel_EyeColor *geneticPredictionModels.NeuralNetwork
var predictionModelMutex_EyeColor sync.Mutex

var predictionModel_LactoseTolerance *geneticPredictionModels.NeuralNetwork
var predictionModelMutex_LactoseTolerance sync.Mutex

var predictionModel_Height *geneticPredictionModels.NeuralNetwork
var predictionModelMutex_Height sync.Mutex

var predictionModel_Autism *geneticPredictionModels.NeuralNetwork
var predictionModelMutex_Autism sync.Mutex

var predictionModel_Homosexualness *geneticPredictionModels.NeuralNetwork
var predictionModelMutex_Homosexualness sync.Mutex

var predictionModel_Obesity *geneticPredictionModels.NeuralNetwork
var predictionModelMutex_Obesity sync.Mutex


// These are the AttributeGenomeTensorConversionGuide files:

//go:embed attributeGenomeTensorConversionGuides/EyeColorGenomeTensorConversionGuide.gob
var attributeGenomeTensorConversionGuideFile_EyeColor []byte

//go:embed attributeGenomeTensorConversionGuides/LactoseToleranceGenomeTensorConversionGuide.gob
var attributeGenomeTensorConversionGuideFile_LactoseTolerance []byte

//go:embed attributeGenomeTensorConversionGuides/HeightGenomeTensorConversionGuide.gob
var attributeGenomeTensorConversionGuideFile_Height []byte

//go:embed attributeGenomeTensorConversionGuides/AutismGenomeTensorConversionGuide.gob
var attributeGenomeTensorConversionGuideFile_Autism []byte

//go:embed attributeGenomeTensorConversionGuides/HomosexualnessGenomeTensorConversionGuide.gob
var attributeGenomeTensorConversionGuideFile_Homosexualness []byte

//go:embed attributeGenomeTensorConversionGuides/ObesityGenomeTensorConversionGuide.gob
var attributeGenomeTensorConversionGuideFile_Obesity []byte


// These are the AttributeGenomeTensorConversionGuide objects
// We need these to make predictions from the models
// They provide instructions to convert genomes into tensors

var attributeGenomeTensorConversionGuide_EyeColor geneticPrediction.AttributeGenomeTensorConversionGuide

var attributeGenomeTensorConversionGuide_LactoseTolerance geneticPrediction.AttributeGenomeTensorConversionGuide

var attributeGenomeTensorConversionGuide_Height geneticPrediction.AttributeGenomeTensorConversionGuide

var attributeGenomeTensorConversionGuide_Autism geneticPrediction.AttributeGenomeTensorConversionGuide

var attributeGenomeTensorConversionGuide_Homosexualness geneticPrediction.AttributeGenomeTensorConversionGuide

var attributeGenomeTensorConversionGuide_Obesity geneticPrediction.AttributeGenomeTensorConversionGuide


// These are the discrete trait prediction model accuracy files:

//go:embed predictionModelAccuracies/EyeColorModelAccuracy.gob
var predictionAccuracyFile_EyeColor []byte

//go:embed predictionModelAccuracies/LactoseToleranceModelAccuracy.gob
var predictionAccuracyFile_LactoseTolerance []byte


// These are the discrete trait prediction model accuracy maps

var predictionAccuracyMap_EyeColor DiscreteTraitPredictionAccuracyInfoMap
var predictionAccuracyMap_LactoseTolerance DiscreteTraitPredictionAccuracyInfoMap


// These are the numeric attribute prediction model accuracy files:

//go:embed predictionModelAccuracies/HeightModelAccuracy.gob
var predictionAccuracyFile_Height []byte

//go:embed predictionModelAccuracies/AutismModelAccuracy.gob
var predictionAccuracyFile_Autism []byte

//go:embed predictionModelAccuracies/HomosexualnessModelAccuracy.gob
var predictionAccuracyFile_Homosexualness []byte

//go:embed predictionModelAccuracies/ObesityModelAccuracy.gob
var predictionAccuracyFile_Obesity []byte

// These are the numeric attribute prediction model accuracy info objects

var predictionAccuracyInfo_Height NumericAttributePredictionAccuracyInfo
var predictionAccuracyInfo_Autism NumericAttributePredictionAccuracyInfo
var predictionAccuracyInfo_Homosexualness NumericAttributePredictionAccuracyInfo
var predictionAccuracyInfo_Obesity NumericAttributePredictionAccuracyInfo


// This function has to be called once upon application startup
// We must also call it before certain tests
func InitializeTrainedPredictionModels()error{

	// We first initialize the neural networks

	attributeNamesList := []string{"Eye Color", "Lactose Tolerance", "Height", "Autism", "Obesity", "Homosexualness"}

	for _, attributeName := range attributeNamesList{

		//Outputs:
		//	-[]byte: Prediction model file
		//	-[]byte: GenomeTensorConversionGuide file
		//	-error
		getPredictionModelFiles := func()([]byte, []byte, error){

			switch attributeName{

				case "Eye Color":{
					return predictionModelFile_EyeColor, attributeGenomeTensorConversionGuideFile_EyeColor, nil
				}
				case "Lactose Tolerance":{
					return predictionModelFile_LactoseTolerance, attributeGenomeTensorConversionGuideFile_LactoseTolerance, nil
				}
				case "Height":{
					return predictionModelFile_Height, attributeGenomeTensorConversionGuideFile_Height, nil
				}
				case "Autism":{
					return predictionModelFile_Autism, attributeGenomeTensorConversionGuideFile_Autism, nil
				}
				case "Obesity":{
					return predictionModelFile_Obesity, attributeGenomeTensorConversionGuideFile_Obesity, nil
				}
				case "Homosexualness":{
					return predictionModelFile_Homosexualness, attributeGenomeTensorConversionGuideFile_Homosexualness, nil
				}
			}

			return nil, nil, errors.New("Trying to initialize genetic prediction model with unknown attributeName: " + attributeName)
		}

		predictionModelFileBytes, genomeTensorConversionGuideFileBytes, err := getPredictionModelFiles()
		if (err != nil) { return err }

		neuralNetworkObject, err := geneticPredictionModels.DecodeBytesToNeuralNetworkObject(predictionModelFileBytes)
		if (err != nil) { return err }

		genomeTensorConversionGuideObject, err :=  geneticPrediction.DecodeBytesToAttributeGenomeTensorConversionGuideObject(genomeTensorConversionGuideFileBytes)
		if (err != nil) { return err }

		switch attributeName{

			case "Eye Color":{
				predictionModel_EyeColor = &neuralNetworkObject
				attributeGenomeTensorConversionGuide_EyeColor = genomeTensorConversionGuideObject
				continue
			}
			case "Lactose Tolerance":{
				predictionModel_LactoseTolerance = &neuralNetworkObject
				attributeGenomeTensorConversionGuide_LactoseTolerance = genomeTensorConversionGuideObject
				continue
			}
			case "Height":{
				predictionModel_Height = &neuralNetworkObject
				attributeGenomeTensorConversionGuide_Height = genomeTensorConversionGuideObject
				continue
			}
			case "Autism":{
				predictionModel_Autism = &neuralNetworkObject
				attributeGenomeTensorConversionGuide_Autism = genomeTensorConversionGuideObject
				continue
			}
			case "Obesity":{
				predictionModel_Obesity = &neuralNetworkObject
				attributeGenomeTensorConversionGuide_Obesity = genomeTensorConversionGuideObject
				continue
			}
			case "Homosexualness":{
				predictionModel_Homosexualness = &neuralNetworkObject
				attributeGenomeTensorConversionGuide_Homosexualness = genomeTensorConversionGuideObject
				continue
			}
		}

		return errors.New("Trying to initialize genetic prediction model with unknown attributeName: " + attributeName)
	}

	// Now we initialize the prediction accuracy information
	// We start with discrete traits

	discreteTraitNamesList := []string{"Eye Color", "Lactose Tolerance"}

	for _, traitName := range discreteTraitNamesList{

		getPredictionAccuracyFileBytes := func()([]byte, error){

			switch traitName{
				case "Eye Color":{
					return predictionAccuracyFile_EyeColor, nil
				}
				case "Lactose Tolerance":{
					return predictionAccuracyFile_LactoseTolerance, nil
				}
			}

			return nil, errors.New("Prediction accuracy file not found for discrete trait: " + traitName)
		}

		predictionAccuracyFileBytes, err := getPredictionAccuracyFileBytes()
		if (err != nil) { return err }

		// We convert the gob encoded file to a map

		discreteTraitPredictionAccuracyInfoMap, err := decodeBytesToDiscreteTraitPredictionAccuracyInfoMap(predictionAccuracyFileBytes)
		if (err != nil) { return err }

		// We initialize the global variables

		switch traitName{
			case "Eye Color":{
				predictionAccuracyMap_EyeColor = discreteTraitPredictionAccuracyInfoMap
				continue
			}
			case "Lactose Tolerance":{
				predictionAccuracyMap_LactoseTolerance = discreteTraitPredictionAccuracyInfoMap
				continue
			}
		}

		return errors.New("Unknown discrete trait name: " + traitName)
	}

	// Now we process numeric attributes

	numericAttributeNamesList := []string{"Height", "Autism", "Homosexualness", "Obesity"}
	
	for _, traitName := range numericAttributeNamesList{

		getPredictionAccuracyFileBytes := func()([]byte, error){

			switch traitName{
				case "Height":{
					return predictionAccuracyFile_Height, nil
				}
				case "Autism":{
					return predictionAccuracyFile_Autism, nil
				}
				case "Homosexualness":{
					return predictionAccuracyFile_Homosexualness, nil
				}
				case "Obesity":{
					return predictionAccuracyFile_Obesity, nil
				}
			}

			return nil, errors.New("Prediction accuracy file not found for numeric trait: " + traitName)
		}

		predictionAccuracyFileBytes, err := getPredictionAccuracyFileBytes()
		if (err != nil) { return err }

		// We convert the gob encoded file to an info object

		numericTraitPredictionAccuracyInfo, err := decodeBytesToNumericAttributePredictionAccuracyInfo(predictionAccuracyFileBytes)
		if (err != nil) { return err }

		// We initialize the global variables

		switch traitName{
			case "Height":{
				predictionAccuracyInfo_Height = numericTraitPredictionAccuracyInfo
				continue
			}
			case "Autism":{
				predictionAccuracyInfo_Autism = numericTraitPredictionAccuracyInfo
				continue
			}
			case "Homosexualness":{
				predictionAccuracyInfo_Homosexualness = numericTraitPredictionAccuracyInfo
				continue
			}
			case "Obesity":{
				predictionAccuracyInfo_Obesity = numericTraitPredictionAccuracyInfo
				continue
			}
		}

		return errors.New("Unknown numeric trait name: " + traitName)
	}

	return nil
}

// We use this to check if a neural network exists for an attribute
func CheckIfAttributeNeuralNetworkExists(attributeName string)bool{

	switch attributeName{

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

			return true
		}
	}

	return false
}


//Outputs:
//	-bool: Neural network model exists for this trait (trait prediction is possible for this trait)
//	-bool: Trait prediction is possible for this user (User has at least 1 known trait locus value)
//	-string: Predicted trait outcome (Example: "Blue")
//	-int: Confidence: Probability (0-100) that the prediction is accurate
//	-int: Quantity of loci known
//	-int: Quantity of phased loci
//	-error
func GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap(traitName string, traitRSIDsList []int64, genomeMap map[int64]locusValue.LocusValue)(bool, bool, string, int, int, int, error){

	//Outputs:
	//	-bool: Prediction model exists
	//	-*geneticPredictionModels.NeuralNetwork
	//	-geneticPrediction.AttributeGenomeTensorConversionGuide
	getPredictionModelAndGenomeTensorConversionGuideObjects := func()(bool, *geneticPredictionModels.NeuralNetwork, geneticPrediction.AttributeGenomeTensorConversionGuide){

		switch traitName{

			case "Eye Color":{
				return true, predictionModel_EyeColor, attributeGenomeTensorConversionGuide_EyeColor
			}
			case "Lactose Tolerance":{
				return true, predictionModel_LactoseTolerance, attributeGenomeTensorConversionGuide_LactoseTolerance
			}
		}

		return false, nil, geneticPrediction.AttributeGenomeTensorConversionGuide{}
	}

	predictionModelExists, predictionModelObject, predictionModelGenomeTensorConversionGuide := getPredictionModelAndGenomeTensorConversionGuideObjects()
	if (predictionModelExists == false){
		// Neural network trait prediction is not possible for this trait
		return false, false, "", 0, 0, 0, nil
	}

	if (predictionModelObject == nil){
		return false, false, "", 0, 0, 0, errors.New("GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap called when trained prediction models are not initialized.")
	}
	if (predictionModelGenomeTensorConversionGuide.QuantityOfInputNeurons == 0){
		return false, false, "", 0, 0, 0, errors.New("GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap called when genome tensor conversion guides are not initialized.")
	}

	if (len(traitRSIDsList) == 0){
		return false, false, "", 0, 0, 0, errors.New("GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap called with empty traitRSIDsList for trait with a neural network.")
	}

	traitRSIDsListCopy := slices.Clone(traitRSIDsList)
	slices.Sort(traitRSIDsListCopy)

	anyLocusIsKnown, neuralNetworkInput, quantityOfLociKnown, quantityOfPhasedLoci, err := geneticPrediction.CreateInputNeuralNetworkLayerFromGenomeMap(predictionModelGenomeTensorConversionGuide, traitRSIDsListCopy, genomeMap)
	if (err != nil) { return false, false, "", 0, 0, 0, err }

	if (anyLocusIsKnown == false){
		// We can't predict anything about this trait for this genome
		return true, false, "", 0, 0, 0, nil
	}

	getPredictionOutcome := func()(string, error){

		// We lock the mutex for the prediction model

		switch traitName{

			case "Eye Color":{
				predictionModelMutex_EyeColor.Lock()
				defer predictionModelMutex_EyeColor.Unlock()
			}
			case "Lactose Tolerance":{
				predictionModelMutex_LactoseTolerance.Lock()
				defer predictionModelMutex_LactoseTolerance.Unlock()
			}
			default:{
				return "", errors.New("traitName not found: " + traitName)
			}
		}

		outputLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(predictionModelObject, false, neuralNetworkInput)
		if (err != nil) { return "", err }

		predictedOutcomeName, err := geneticPrediction.GetDiscreteOutcomeNameFromOutputLayer(traitName, false, outputLayer)
		if (err != nil) { return "", err }

		return predictedOutcomeName, nil
	}

	predictedOutcome, err := getPredictionOutcome()
	if (err != nil) { return false, false, "", 0, 0, 0, err }

	//Outputs:
	//	-int: Prediction Accuracy (0-100)
	//	-error
	getPredictionAccuracy := func()(int, error){

		// Now we find the prediction accuracy
		// This is accomplished by finding at least 10 predictions that are as close as
		//		possible to our prediction's percentageOfLociTested and prcentageOfPhasedLoci
		// Then we see what the average probability is that those predictions are actually correct

		totalNumberOfTraitLoci := len(traitRSIDsList)

		proportionOfLociTested := float64(quantityOfLociKnown)/float64(totalNumberOfTraitLoci)
		percentageOfLociTested := int(proportionOfLociTested * 100)

		proportionOfPhasedLoci := float64(quantityOfPhasedLoci)/float64(totalNumberOfTraitLoci)
		percentageOfPhasedLoci := int(proportionOfPhasedLoci * 100)

		// First we create a map which will store every prediction accuracy probability/quantity of predictions for
		//		the offset distances from our target percentageOfLociTested and percentageOfPhasedLoci.

		// Each offsetDistance is a value that represents the distance our closest prediction accuracy has from the current prediction
		// Consider each prediction accuracy value on an (X,Y) coordinate plane
		// X = Number of loci tested
		// Y = Number of phased loci

		type OutputPredictionAccuracyInfo struct{
			ProbabilityOfCorrectPrediction int
			QuantityOfPredictions int
		}

		// Map structure: Prediction offset distance -> []OutputPredictionAccuracyInfo
		predictionAccuraciesMap := make(map[float64][]OutputPredictionAccuracyInfo)

		modelTraitAccuracyInfoMap, err := GetPredictionModelDiscreteTraitAccuracyInfoMap(traitName)
		if (err != nil) { return 0, err }

		for traitOutcomeInfo, traitPredictionAccuracyInfo := range modelTraitAccuracyInfoMap{

			outcomeName := traitOutcomeInfo.OutcomeName
			if (outcomeName != predictedOutcome){
				continue
			}

			probabilityOfCorrectPrediction := traitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Output
			quantityOfPredictions := traitPredictionAccuracyInfo.QuantityOfPredictions_Output
			currentPercentageOfLociTested := traitOutcomeInfo.PercentageOfLociTested
			currentPercentageOfPhasedLoci := traitOutcomeInfo.PercentageOfPhasedLoci

			// Distance Formula for 2 coordinates (x1, y1) and (x2, y2):
			//	distance = √((x2 - x1)^2 + (y2 - y1)^2)

			differenceInX := float64(currentPercentageOfLociTested - percentageOfLociTested)
			differenceInY := float64(currentPercentageOfPhasedLoci - percentageOfPhasedLoci)

			distance := math.Sqrt(math.Pow(differenceInX, 2) + math.Pow(differenceInY, 2))

			newOutputPredictionAccuracyInfo := OutputPredictionAccuracyInfo{
				ProbabilityOfCorrectPrediction: probabilityOfCorrectPrediction,
				QuantityOfPredictions: quantityOfPredictions,
			}

			existingList, exists := predictionAccuraciesMap[distance]
			if (exists == false){
				newList := []OutputPredictionAccuracyInfo{newOutputPredictionAccuracyInfo}
				predictionAccuraciesMap[distance] = newList
			} else {
				existingList = append(existingList, newOutputPredictionAccuracyInfo)
			}
		}

		// Now we create a list of all prediction offset distances

		distancesList := helpers.GetListOfMapKeys(predictionAccuraciesMap)

		// We sort the distances in ascending order to find the most relevant prediction accuracy probabilities

		slices.Sort(distancesList)

		quantityOfPredictions := 0
		quantityOfCorrectPredictions := 0

		for _, distance := range distancesList{

			outputPredictionAccuracyInfoList, exists := predictionAccuraciesMap[distance]
			if (exists == false){
				return 0, errors.New("predictionAccuraciesMap missing entry for distance.")
			}

			for _, outputPredictionAccuracyInfo := range outputPredictionAccuracyInfoList{

				currentPercentageProbabilityOfCorrectPrediction := outputPredictionAccuracyInfo.ProbabilityOfCorrectPrediction
				currentQuantityOfPredictions := outputPredictionAccuracyInfo.QuantityOfPredictions

				currentProbabilityOfCorrectPrediction := float64(currentPercentageProbabilityOfCorrectPrediction)/100

				currentQuantityOfCorrectPredictions := int(float64(currentQuantityOfPredictions) * currentProbabilityOfCorrectPrediction)

				quantityOfPredictions += currentQuantityOfPredictions
				quantityOfCorrectPredictions += currentQuantityOfCorrectPredictions
			}

			if (quantityOfPredictions >= 10){
				break
			}
		}

		if (quantityOfPredictions < 10){
			// There are fewer than 10 predictions for this outcome
			// Thus, we do not know the real-world accuracy.
			// We say the accuracy is 0.
			return 0, nil
		}

		// This is the percentage probability of a correct prediction
		averagePredictionAccuracy := int((float64(quantityOfCorrectPredictions)/float64(quantityOfPredictions))*100)

		return averagePredictionAccuracy, nil
	}

	predictionAccuracy, err := getPredictionAccuracy()
	if (err != nil){ return false, false, "", 0, 0, 0, err }

	return true, true, predictedOutcome, predictionAccuracy, quantityOfLociKnown, quantityOfPhasedLoci, nil
}


// This function is used to predict numeric traits and polygenic disease risk scores
//Outputs:
//	-bool: Neural network model exists for this attribute (neural network prediction is possible for this attribute)
//	-bool: Attribute prediction is possible for this user (User has at least 1 known attribute locus value for loci used by the prediction model)
//	-float64: Predicted attribute outcome (Example: Height in centimeters)
//	-map[int]float64: Prediction accuracy ranges map
//		-Map Structure: Probability prediction is accurate (X) -> Distance from prediction that must be travelled in both directions to
//			create a range in which the true value will fall into, X% of the time
// 		For example, if the model predicted that someone was 150 centimeters tall,
//			how many centimeters would we have to deviate in both directions
//			in order for the true outcome to fall into the range 10% of the time, 20% of the time, 30% of the time, etc...
// 		Example:
//			-90%+: 50 centimeters
//				If you travel 50 centimeters in both directions from the prediction,
//					the true height value will fall into this range 90% of the time.
//			-50%+: 20 centimeters
//			-10%+: 10 centimeters
//	-int: Quantity of loci known
//	-int: Quantity of phased loci
//	-error
func GetNeuralNetworkNumericAttributePredictionFromGenomeMap(attributeName string, attributeLociList []int64, genomeMap map[int64]locusValue.LocusValue)(bool, bool, float64, map[int]float64, int, int, error){

	//Outputs:
	//	-bool: Prediction model exists
	//	-*geneticPredictionModels.NeuralNetwork
	//	-geneticPrediction.AttributeGenomeTensorConversionGuide
	getPredictionModelAndGenomeTensorConversionGuideObjects := func()(bool, *geneticPredictionModels.NeuralNetwork, geneticPrediction.AttributeGenomeTensorConversionGuide){

		switch attributeName{

			case "Height":{
				return true, predictionModel_Height, attributeGenomeTensorConversionGuide_Height
			}
			case "Autism":{
				return true, predictionModel_Autism, attributeGenomeTensorConversionGuide_Autism
			}
			case "Obesity":{
				return true, predictionModel_Obesity, attributeGenomeTensorConversionGuide_Obesity
			}
			case "Homosexualness":{
				return true, predictionModel_Homosexualness, attributeGenomeTensorConversionGuide_Homosexualness
			}
		}

		return false, nil, geneticPrediction.AttributeGenomeTensorConversionGuide{}
	}

	predictionModelExists, predictionModelObject, attributeGenomeTensorConversionGuide := getPredictionModelAndGenomeTensorConversionGuideObjects()
	if (predictionModelExists == false){
		// Neural network trait prediction is not possible for this trait
		return false, false, 0, nil, 0, 0, nil
	}

	if (predictionModelObject == nil){
		return false, false, 0, nil, 0, 0, errors.New("GetNeuralNetworkNumericAttributePredictionFromGenomeMap called when trained prediction models are not initialized.")
	}
	if (attributeGenomeTensorConversionGuide.QuantityOfInputNeurons == 0){
		return false, false, 0, nil, 0, 0, errors.New("GetNeuralNetworkNumericAttributePredictionFromGenomeMap called when attributeGenomeTensorConversionGuides are not initialized.")
	}

	if (len(attributeLociList) == 0){
		return false, false, 0, nil, 0, 0, errors.New("GetNeuralNetworkNumericAttributePredictionFromGenomeMap called with empty attributeLociList for an attribute with a neural network.")
	}

	attributeLociListCopy := slices.Clone(attributeLociList)

	anyLociValuesKnown, neuralNetworkInput, quantityOfLociKnown, quantityOfPhasedLoci, err := geneticPrediction.CreateInputNeuralNetworkLayerFromGenomeMap(attributeGenomeTensorConversionGuide, attributeLociListCopy, genomeMap)
	if (err != nil) { return false, false, 0, nil, 0, 0, err }

	if (anyLociValuesKnown == false){
		// We can't predict anything about this attribute for this genome
		return true, false, 0, nil, 0, 0, nil
	}

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

		// We lock the mutex for the prediction model

		switch attributeName{

			case "Height":{
				predictionModelMutex_Height.Lock()
				defer predictionModelMutex_Height.Unlock()
			}
			case "Autism":{
				predictionModelMutex_Autism.Lock()
				defer predictionModelMutex_Autism.Unlock()
			}
			case "Obesity":{
				predictionModelMutex_Obesity.Lock()
				defer predictionModelMutex_Obesity.Unlock()
			}
			case "Homosexualness":{
				predictionModelMutex_Homosexualness.Lock()
				defer predictionModelMutex_Homosexualness.Unlock()
			}
			default:{
				return 0, errors.New("attributeName not found: " + attributeName)
			}
		}

		outputLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(predictionModelObject, true, neuralNetworkInput)
		if (err != nil) { return 0, err }

		predictedOutcomeValue, err := geneticPrediction.GetNumericOutcomeValueFromOutputLayer(attributeName, outputLayer)
		if (err != nil) { return 0, err }

		return predictedOutcomeValue, nil
	}

	predictedOutcome, err := getPredictionOutcome()
	if (err != nil) { return false, false, 0, nil, 0, 0, err }

	modelAccuracyInfo, err := GetPredictionModelNumericAttributeAccuracyInfo(attributeName)
	if (err != nil) { return false, false, 0, nil, 0, 0, err }

	totalNumberOfAttributeLoci := len(attributeLociListCopy)

	proportionOfLociTested := float64(quantityOfLociKnown)/float64(totalNumberOfAttributeLoci)
	percentageOfLociTested := int(proportionOfLociTested * 100)

	proportionOfPhasedLoci := float64(quantityOfPhasedLoci)/float64(totalNumberOfAttributeLoci)
	percentageOfPhasedLoci := int(proportionOfPhasedLoci * 100)

	// We create a prediction confidence ranges map for our prediction

	predictionAccuracyInfoMap_Output := modelAccuracyInfo.PredictionAccuracyInfoMap_Output

	// We first cycle through all of the predictions and all them to a map
	// TODO: Use the predicted value and compare it to the predictedValue from each prediction to improve confidence range accuracy

	// Each offsetDistance is a value that represents the distance our closest prediction accuracy has from the current prediction
	// Consider each prediction accuracy value on an (X,Y) coordinate plane
	// X = Number of loci tested
	// Y = Number of phased loci

	// Map Structure: Offset distance -> Every prediction distance for predictions with this offset
	predictionDistancesMap := make(map[float64][]float64)

	for predictionAccuracyInfo, distancesList := range predictionAccuracyInfoMap_Output{

		currentPercentageOfLociTested := predictionAccuracyInfo.PercentageOfLociTested
		currentPercentageOfPhasedLoci := predictionAccuracyInfo.PercentageOfPhasedLoci

		// Distance Formula for 2 coordinates (x1, y1) and (x2, y2):
		//	distance = √((x2 - x1)^2 + (y2 - y1)^2)

		differenceInX := float64(currentPercentageOfLociTested - percentageOfLociTested)
		differenceInY := float64(currentPercentageOfPhasedLoci - percentageOfPhasedLoci)

		offsetDistance := math.Sqrt(math.Pow(differenceInX, 2) + math.Pow(differenceInY, 2))

		existingList, exists := predictionDistancesMap[offsetDistance]
		if (exists == false){

			// We copy the slice so we do not alter the original later
			distancesListCopy := slices.Clone(distancesList)

			predictionDistancesMap[offsetDistance] = distancesListCopy
		} else {
			existingList = append(existingList, distancesList...)
			predictionDistancesMap[offsetDistance] = existingList
		}
	}

	offsetDistancesList := helpers.GetListOfMapKeys(predictionDistancesMap)

	// Sort in ascending order
	slices.Sort(offsetDistancesList)

	// We sort and cycle through all of the offset distances and append all the distances for predictions, starting with the shortest offset
	// We check to see if there are enough examples in this list (>10)
	// If yes, we use this list of distances to construct a ConfidenceRangesMap
	// If no, keep going, reaching further offset distances until we have enough examples 

	relevantDistancesList := make([]float64, 0)

	for _, offsetDistance := range offsetDistancesList{

		predictionsList, exists := predictionDistancesMap[offsetDistance]
		if (exists == false){
			return false, false, 0, nil, 0, 0, errors.New("predictionDistancesMap missing offsetDistance entry.")
		}

		relevantDistancesList = append(relevantDistancesList, predictionsList...)

		if (len(relevantDistancesList) >= 10){
			break
		}
	}

	if (len(relevantDistancesList) < 10){
		// This should never happen
		return false, false, 0, nil, 0, 0, errors.New("predictionAccuracyInfoMap_Output contains fewer than 10 examples.")
	}

	// We have retrieved the distances to use to create our confidenceRangesMap

	newConfidenceRangesMap := make(map[int]float64)

	// We sort the distances in ascending order
	slices.Sort(relevantDistancesList)

	finalIndex := len(relevantDistancesList) - 1

	for index, distance := range relevantDistancesList{

		if (index == 0){
			// 0% accuracy is not a useful metric for users
			continue
		}

		proportionOfPredictionsWithinDistance := float64(index)/float64(finalIndex)

		percentageOfPredictionsWithinDistance := int(100 * proportionOfPredictionsWithinDistance)

		_, exists := newConfidenceRangesMap[percentageOfPredictionsWithinDistance]
		if (exists == true){
			// There exists a value for this percentage already
			// This happens because we convert a float64 to an int
			// The existing percentage must be smaller than our current percentage
			// We want to keep that smaller percentage
			// For example, we would rather keep the 15.1% value than the 15.8% value.
			continue
		}

		newConfidenceRangesMap[percentageOfPredictionsWithinDistance] = distance
	}

	return true, true, predictedOutcome, newConfidenceRangesMap, quantityOfLociKnown, quantityOfPhasedLoci, nil
}



// This map is used to store information about how accurate genetic prediction models are for discrete traits
// Map Structure: Discrete Trait Outcome Info -> Discrete Trait Prediction Accuracy Info
type DiscreteTraitPredictionAccuracyInfoMap map[DiscreteTraitOutcomeInfo]DiscreteTraitPredictionAccuracyInfo

type DiscreteTraitOutcomeInfo struct{

	// This is the outcome which was predicted
	// Example: "Blue"
	OutcomeName string

	// This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction
	PercentageOfLociTested int

	// This is a value between 0-100 which describes the percentage of the tested loci which were phased for the input for the prediction
	PercentageOfPhasedLoci int
}

type DiscreteTraitPredictionAccuracyInfo struct{

	// This contains the quantity of input examples for the outcome with the specified percentageOfLociTested and percentageOfPhasedLoci
	// Input = the training data example is for a person who actually had this outcome in real life
	QuantityOfPredictions_Input int

	// This contains the quantity of predictions for the outcome with the specified percentageOfLociTested and percentageOfPhasedLoci
	// Output = our model predicted/outputted this outcome for a supplied input genome
	QuantityOfPredictions_Output int

	// This stores the probability (0-100) that our model will accurately predict this outcome for a genome which has
	//		the specified percentageOfLociTested and percentageOfPhasedLoci
	// In other words: What is the probability that if you give Seekia a genome which belongs to a blue-eyed person,
	//		Seekia will give you a correct Blue prediction?
	// This value is only accurate if QuantityOfPredictions_Input > 0
	ProbabilityOfCorrectPrediction_Input int

	// This stores the probability (0-100) that our model is correct if our model predicts that a genome
	//		with the specified percentageOfLociTested and percentageOfPhasedLoci has this outcome
	// In other words: What is the probability that if Seekia says a genome will have blue eyes, it is correct?
	// This value is only accurate if QuantityOfPredictions_Output > 0
	ProbabilityOfCorrectPrediction_Output int
}

func EncodeDiscreteTraitPredictionAccuracyInfoMapToBytes(inputMap DiscreteTraitPredictionAccuracyInfoMap)([]byte, error){

	buffer := new(bytes.Buffer)

	encoder := gob.NewEncoder(buffer)

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

	inputMapBytes := buffer.Bytes()

	return inputMapBytes, nil
}

func decodeBytesToDiscreteTraitPredictionAccuracyInfoMap(inputBytes []byte)(DiscreteTraitPredictionAccuracyInfoMap, error){

	if (inputBytes == nil){
		return nil, errors.New("DecodeBytesToDiscreteTraitPredictionAccuracyInfoMap called with nil inputBytes.")
	}

	buffer := bytes.NewBuffer(inputBytes)

	decoder := gob.NewDecoder(buffer)

	var newDiscreteTraitPredictionAccuracyInfoMap DiscreteTraitPredictionAccuracyInfoMap

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

	return newDiscreteTraitPredictionAccuracyInfoMap, nil
}


type NumericAttributePredictionAccuracyInfo struct{

	// There are 2 prediction accuracy info maps:
	//	1. Input
	//	2. Output
	//
	// The Input prediction accuracy info map allows us to determine the accuracy of predictions for any given input
	// 		For example, if we are 6 feet tall, how accurate will the prediction from the model be?
	// The Output prediction accuracy info map allows us to determine the accuracy of predictions for any given genome
	// 		For example, if the prediction model says someone is 6 feet tall, how accurate is that prediction for any given genome?

	// We need both of these because we need to account for a model that is correct most of the time but wrong for the edge cases
	// For example, if 90% of people were 5 feet tall, and our prediction model always predicted 5 feet tall,
	//	our model would be 90% accurate for all genomes, but 0% accurate for people who were not 5 feet tall.
	// Thus, we need a way of describing the accuracy of predictions for people of different heights.

	PredictionAccuracyInfoMap_Input NumericAttributePredictionAccuracyInfoMap_Input

	PredictionAccuracyInfoMap_Output NumericAttributePredictionAccuracyInfoMap_Output
}


// Map Structure: NumericAttributePredictionInfo_Input -> Distance from true result for each prediction
type NumericAttributePredictionAccuracyInfoMap_Input map[NumericAttributePredictionInfo_Input][]float64

type NumericAttributePredictionInfo_Input struct{

	// This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction
	PercentageOfLociTested int

	// This is a value between 0-100 which describes the percentage of the tested loci which were phased for the input for the prediction
	PercentageOfPhasedLoci int

	// The actual, true, expected value that the prediction is trying to match
	// Examples: Height, obesity index, autism index, etc....
	ExpectedValue float64
}

// Map Structure: NumericAttributePredictionInfo_Output -> Distance from true result for each prediction
type NumericAttributePredictionAccuracyInfoMap_Output map[NumericAttributePredictionInfo_Output][]float64

type NumericAttributePredictionInfo_Output struct{

	// This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction
	PercentageOfLociTested int

	// This is a value between 0-100 which describes the percentage of the tested loci which were phased for the input for the prediction
	PercentageOfPhasedLoci int

	// The value that was predicted by the model
	// Examples: Height, obesity index, autism index, etc....
	PredictedValue float64
}

func EncodeNumericAttributePredictionAccuracyInfoToBytes(inputInfo NumericAttributePredictionAccuracyInfo)([]byte, error){

	buffer := new(bytes.Buffer)

	encoder := gob.NewEncoder(buffer)

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

	inputMapBytes := buffer.Bytes()

	return inputMapBytes, nil
}

func decodeBytesToNumericAttributePredictionAccuracyInfo(inputBytes []byte)(NumericAttributePredictionAccuracyInfo, error){

	if (inputBytes == nil){
		return NumericAttributePredictionAccuracyInfo{}, errors.New("DecodeBytesToNumericAttributePredictionAccuracyInfo called with nil inputBytes.")
	}

	buffer := bytes.NewBuffer(inputBytes)

	decoder := gob.NewDecoder(buffer)

	var newNumericAttributePredictionAccuracyInfo NumericAttributePredictionAccuracyInfo

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

	return newNumericAttributePredictionAccuracyInfo, nil
}



func GetPredictionModelDiscreteTraitAccuracyInfoMap(traitName string)(DiscreteTraitPredictionAccuracyInfoMap, error){

	getAccuracyInfoMap := func()(DiscreteTraitPredictionAccuracyInfoMap, error){

		switch traitName{
			case "Eye Color":{
				return predictionAccuracyMap_EyeColor, nil
			}
			case "Lactose Tolerance":{
				return predictionAccuracyMap_LactoseTolerance, nil
			}
		}

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

	accuracyInfoMap, err := getAccuracyInfoMap()
	if (err != nil) { return nil, err }

	if (accuracyInfoMap == nil){
		return nil, errors.New("GetPredictionModelDiscreteTraitAccuracyInfoMap called when map is not initialized.")
	}

	return accuracyInfoMap, nil
}


// The files returned by this function are .gob encoded geneticPrediction.NumericAttributePredictionAccuracyInfo objects
func GetPredictionModelNumericAttributeAccuracyInfo(attributeName string)(NumericAttributePredictionAccuracyInfo, error){

	getAccuracyInfo := func()(NumericAttributePredictionAccuracyInfo, error){

		switch attributeName{
			case "Height":{
				return predictionAccuracyInfo_Height, nil
			}
			case "Autism":{
				return predictionAccuracyInfo_Autism, nil
			}
			case "Homosexualness":{
				return predictionAccuracyInfo_Homosexualness, nil
			}
			case "Obesity":{
				return predictionAccuracyInfo_Obesity, nil
			}
		}

		return NumericAttributePredictionAccuracyInfo{}, errors.New("GetPredictionModelNumericAttributeAccuracyInfo called with unknown attributeName: " + attributeName)
	}

	accuracyInfo, err := getAccuracyInfo()
	if (err != nil) { return NumericAttributePredictionAccuracyInfo{}, err }

	// We check to see if the accuracyInfo has been initialized

	accuracyInfoMap_Input := accuracyInfo.PredictionAccuracyInfoMap_Input

	if (accuracyInfoMap_Input == nil){
		return NumericAttributePredictionAccuracyInfo{}, errors.New("GetPredictionModelNumericAttributeAccuracyInfo called when info is not initialized.")
	}

	return accuracyInfo, nil
}


