
// createGeneticModels.go provides an interface to create genetic prediction models
// These are neural networks which predict attributes such as eye color and autism from raw genome files
// The OpenSNP.org dataset is used, and more datasets will be added in the future.
// You must download the dataset and extract it. The instructions are described in the utility.
// The trained models are saved in the /internal/genetics/trainedPredictionModels package for use in the Seekia app.

package main

import "fyne.io/fyne/v2"
import "fyne.io/fyne/v2/app"
import "fyne.io/fyne/v2/canvas"
import "fyne.io/fyne/v2/widget"
import "fyne.io/fyne/v2/container"
import "fyne.io/fyne/v2/theme"
import "fyne.io/fyne/v2/layout"
import "fyne.io/fyne/v2/dialog"
import "fyne.io/fyne/v2/data/binding"

import "seekia/internal/createCharts"
import "seekia/internal/encoding"
import "seekia/internal/genetics/geneticReferences/polygenicDiseases"
import "seekia/internal/genetics/geneticReferences/traits"
import "seekia/internal/genetics/geneticReferences/locusMetadata"
import "seekia/internal/genetics/trainedPredictionModels"
import "seekia/internal/genetics/locusValue"
import "seekia/internal/genetics/prepareRawGenomes"
import "seekia/internal/genetics/readBiobankData"
import "seekia/internal/genetics/readRawGenomes"
import "seekia/internal/genetics/geneticPrediction"
import "seekia/internal/genetics/geneticPredictionModels"
import "seekia/internal/globalSettings"
import "seekia/internal/helpers"
import "seekia/internal/imagery"
import "seekia/internal/localFilesystem"
import "seekia/internal/statisticsDatum"

import zeeboBlake3 "github.com/zeebo/blake3"
import "errors"
import "bytes"
import "image"
import "image/color"
import "io"
import "os"
import "strings"
import "sync"
import "slices"
import "math"
import mathRand "math/rand/v2"
import goFilepath "path/filepath"
import "time"



func main(){

	err := polygenicDiseases.InitializePolygenicDiseaseVariables()
	if (err != nil){
		panic(err)
	}

	err = traits.InitializeTraitVariables()
	if (err != nil){
		panic(err)
	}

	err = globalSettings.InitializeGlobalSettingsDatastore()
	if (err != nil){
		panic(err)
	}

	app := app.NewWithID("CreateGeneticModelsUtility")

	customTheme := getCustomFyneTheme()

	app.Settings().SetTheme(customTheme)

	window := app.NewWindow("Seekia - Create Genetic Models Utility")

	windowSize := fyne.NewSize(600, 600)
	window.Resize(windowSize)

	window.CenterOnScreen()

	setHomePage(window)

	window.ShowAndRun()
}


func getWidgetCentered(widget fyne.Widget)*fyne.Container{
	
	widgetCentered := container.NewHBox(layout.NewSpacer(), widget, layout.NewSpacer())
	
	return widgetCentered
}

func getContainerCentered(inputContainer *fyne.Container)*fyne.Container{
	
	containerCentered := container.NewHBox(layout.NewSpacer(), inputContainer, layout.NewSpacer())

	return containerCentered
}

func getLabelCentered(text string) *fyne.Container{

	label := widget.NewLabel(text)
	labelCentered := container.NewHBox(layout.NewSpacer(), label, layout.NewSpacer())

	return labelCentered
}

func getBoldLabel(text string) fyne.Widget{

	titleStyle := fyne.TextStyle{
		Bold: true,
		Italic: false,
		Monospace: false,
	}

	boldLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), titleStyle)

	return boldLabel
}

func getItalicLabel(text string) fyne.Widget{
	
	italicTextStyle := fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}
	
	italicLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), italicTextStyle)

	return italicLabel
}

func getBoldLabelCentered(inputText string)*fyne.Container{

	boldLabel := getBoldLabel(inputText)

	boldLabelCentered := container.NewHBox(layout.NewSpacer(), boldLabel, layout.NewSpacer())

	return boldLabelCentered
}

func getSelectableBoldLabelCentered(inputText string)*fyne.Container{

	titleStyle := fyne.TextStyle{
		Bold: true,
		Italic: false,
		Monospace: false,
	}

	boldLabel := widget.NewLabelWithStyle(inputText, fyne.TextAlign(fyne.TextAlignCenter), titleStyle)

	boldLabel.Selectable = true

	boldLabelCentered := container.NewHBox(layout.NewSpacer(), boldLabel, layout.NewSpacer())

	return boldLabelCentered
}

func getItalicLabelCentered(inputText string)*fyne.Container{

	italicLabel := getItalicLabel(inputText)

	italicLabelCentered := container.NewHBox(layout.NewSpacer(), italicLabel, layout.NewSpacer())

	return italicLabelCentered
}


func showUnderConstructionDialog(window fyne.Window){

	dialogTitle := "Under Construction"
	dialogMessageA := getLabelCentered("Seekia is under construction.")
	dialogMessageB := getLabelCentered("This page/feature needs to be built.")
	dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
	dialog.ShowCustom(dialogTitle, "Close", dialogContent, window)
}


func getBackButtonCentered(previousPage func())*fyne.Container{

	backButton := getWidgetCentered(widget.NewButtonWithIcon("Go Back", theme.NavigateBackIcon(), previousPage))

	return backButton
}

func setErrorEncounteredPage(window fyne.Window, err error, previousPage func()){

	title := getBoldLabelCentered("Error Encountered")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("Something went wrong. Report this error to Seekia developers.")

	header := container.NewVBox(title, backButton, widget.NewSeparator(), description1, widget.NewSeparator())

	getErrorString := func()string{
		if (err == nil){
			return "No nav bar error encountered page called with nil error."
		}
		errorString := err.Error()
		return errorString
	}

	errorString := getErrorString()

	errorLabel := widget.NewLabel(errorString)
	errorLabel.Wrapping = 3
	errorLabel.Alignment = 1
	errorLabel.Selectable = true
	errorLabel.TextStyle = fyne.TextStyle{
		Bold: true,
		Italic: false,
		Monospace: false,
	}

	page := container.NewBorder(header, nil, nil, nil, errorLabel)

	window.SetContent(page)	
}


// This loading screen shows no progress, so it should only be used when retrieving progress is impossible
func setLoadingScreen(window fyne.Window, pageTitle string, loadingText string){

	title := getBoldLabelCentered(pageTitle)

	loadingLabel := getWidgetCentered(getItalicLabel(loadingText))
	progressBar := getWidgetCentered(widget.NewProgressBarInfinite())

	pageContent := container.NewVBox(title, loadingLabel, progressBar)

	page := container.NewCenter(pageContent)

	window.SetContent(page)
}


func setHomePage(window fyne.Window){

	currentPage := func(){setHomePage(window)}

	title := getBoldLabelCentered("Create Genetic Models Utility")

	description1 := getLabelCentered("This utility is used to create the genetic prediction models.")
	description2 := getLabelCentered("These models are used to predict attributes such as eye color and autism from raw genome files.")
	description3 := getLabelCentered("Seekia aims to have open source and reproducible genetic prediction technology.")

	step1Label := getLabelCentered("Step 1:")

	downloadDatasetsButton := getWidgetCentered(widget.NewButton("Download Datasets", func(){
			setDownloadDatasetsPage(window, 1, currentPage)
		}))

	step2Label := getLabelCentered("Step 2:")

	extractDatasetsButton := getWidgetCentered(widget.NewButton("Extract Datasets", func(){
			setExtractDatasetsPage(window, 1, currentPage)
		}))

	step3Label := getLabelCentered("Step 3:")

	extractPersonAttributeDataButton := getWidgetCentered(widget.NewButton("Extract Person Attribute Data", func(){
			setExtractPersonAttributeDataPage(window, currentPage)
		}))

	step4Label := getLabelCentered("Step 4:")

	createTrainingDataButton := getWidgetCentered(widget.NewButton("Create Training Data", func(){
			setCreateTrainingDataPage(window, currentPage)
		}))

	step5Label := getLabelCentered("Step 5:")

	trainModelsButton := getWidgetCentered(widget.NewButton("Train Models", func(){
			setTrainModelsPage(window, currentPage)
		}))

	step6Label := getLabelCentered("Step 6:")

	testModelsButton := getWidgetCentered(widget.NewButton("Test Models", func(){
			setTestModelsPage(window, currentPage)
		}))

	//TODO: A page to verify the checksums of the generated .gob models

	page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), step1Label, downloadDatasetsButton, widget.NewSeparator(), step2Label, extractDatasetsButton, widget.NewSeparator(), step3Label, extractPersonAttributeDataButton, widget.NewSeparator(), step4Label, createTrainingDataButton, widget.NewSeparator(), step5Label, trainModelsButton, widget.NewSeparator(), step6Label, testModelsButton)

	window.SetContent(page)
}


func setDownloadDatasetsPage(window fyne.Window, fileIndex int, previousPage func()){

	if (fileIndex != 1 && fileIndex != 2){
		setErrorEncounteredPage(window, errors.New("setDownloadDatasetsPage called with invalid fileIndex."), previousPage)
		return
	}

	currentPage := func(){setDownloadDatasetsPage(window, fileIndex, previousPage)}

	title := getBoldLabelCentered("Download Datasets")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("You must download 2 OpenSNP.org dataset files.")
	description2 := getLabelCentered("These are compressed archives which contain genomes and phenotype information.")
	description3 := getLabelCentered("They are hosted using the BitTorrent decentralized filesharing protocol.")
	description4 := getLabelCentered("You must use a torrenting client to download the files.")
	description5 := getLabelCentered("You can also download the files via other methods if they are shared elsewhere.")

	// Both files are uploaded to this proton drive folder:
	// Link: https://drive.proton.me/urls/6B2S1AB6R8#VpW137MHmCLh
	// Downloading from the folder may be rate-limited or bandwidth-capped.
	// IPFS is the better download option for most people.
 
	fileSelector := widget.NewSelect([]string{"File 1", "File 2"}, func(newSelection string){
			if (newSelection == "File 1"){
				if (fileIndex == 1){
					return
				}
				setDownloadDatasetsPage(window, 1, previousPage)
			} else if (newSelection == "File 2"){
				if (fileIndex == 2){
					return
				}
				setDownloadDatasetsPage(window, 2, previousPage)
			}
		})

	if (fileIndex == 1){
		fileSelector.Selected = "File 1"
	} else if (fileIndex == 2){
		fileSelector.Selected = "File 2"
	}

	fileSelectorCentered := getWidgetCentered(fileSelector)

	// Outputs:
	//	-string: The file name
	//	-string: The SHA-256 Checksum for the file, encoded in Hex
	//	-string: The Blake3 Checksum for the file, encoded in Hex
	//	-string: The .torrent filename
	//	-string: The size of the file, described in a human-readable format
	//	-string: The size of the extracted file, described in a human-readable format
	//	-error
	getFileInfo := func()(string, string, string, string, string, string, error){

		if (fileIndex == 1){
			return "OpenSNPDataArchive1.tar.gz", "49f84fb71cb12df718a80c1ce25f6370ba758cbee8f24bd8a6d4f0da2e3c51ee", "9afde54855abe1b6ba69effae00b153204789c72c4bd7fe42e35679c2d0dd367", "OpenSNPDataArchive1.tar.gz.torrent", "48,961,240 bytes (50.1 GB)", "128,533,341,751 bytes (119.7 GB)", nil
		}
		if (fileIndex == 2){
		return "OpenSNPDataArchive2.tar.gz", "af941d3201503323b8e538eade0ab28d86af8392f7c57078137022fc4b3ac0f1", "2022042a7a4fe0af0efb87b6a2f3ad2fddc86478e95731f31c1643e8dea475f0", "OpenSNPDataArchive2.tar.gz.torrent", "10,670,726,682 bytes (9.9 GB)", "28,256,224,663 bytes (26.3 GB)", nil
		}

		return "", "", "", "", "", "", errors.New("setDownloadDatasetsPage called with invalid fileIndex")
	}

	fileName, fileHashSha256, fileHashBlake3, fileTorrentFilename, fileSizeDescription, extractedFileSizeDescription, err := getFileInfo()
	if (err != nil){
		setErrorEncounteredPage(window, err, previousPage)
		return
	}

	fileNameTitle := getLabelCentered("File Name:")

	fileNameLabel := getSelectableBoldLabelCentered(fileName)

	fileHashTitle := getLabelCentered("File SHA-256 Checksum Hash:")

	fileHashLabel := getSelectableBoldLabelCentered(fileHashSha256)

	torrentFileTitle := getLabelCentered("Torrent File:")

	fileTorrentFilenameLabel := getSelectableBoldLabelCentered(fileTorrentFilename)

	saveTorrentFileButton := getWidgetCentered(widget.NewButtonWithIcon("Save .torrent File", theme.DocumentSaveIcon(), func(){

			saveFileCallbackFunction := func(newFileWriter fyne.URIWriteCloser, callbackError error){

				//Outputs:
				//	-bool: File was saved
				//	-error
				saveFile := func()(bool, error){

					if (callbackError != nil){
						return false, callbackError
					}
					if (newFileWriter == nil){
						return false, nil
					}

					fileExists, fileContents, err := localFilesystem.GetFileContents("./TorrentFiles/" + fileTorrentFilename)
					if (err != nil){ return false, err }
					if (fileExists == false){
						return false, errors.New("File not found: " + fileTorrentFilename)
					}

					_, err = newFileWriter.Write(fileContents)
					if (err != nil){ return false, err }

					err = newFileWriter.Close()
					if (err != nil){ return false, err }

					return true, nil
				}

				fileWasSaved, err := saveFile()
				if (err != nil){
					title := "Error Encountered"
					dialogMessage1 := getLabelCentered("Report this error to the Seekia developers:")
					dialogMessage2 := getLabelCentered("ERROR: " + err.Error())
					dialogContent := container.NewVBox(dialogMessage1, dialogMessage2)
					dialog.ShowCustom(title, "Close", dialogContent, window)
					return
				}
				if (fileWasSaved == false){
					return
				}

				title := "File Saved!"
				dialogMessage1 := getLabelCentered("The " + fileTorrentFilename + " file was successfully saved in your chosen location.")
				dialogContent := container.NewVBox(dialogMessage1)
				dialog.ShowCustom(title, "Close", dialogContent, window)
			}

			newDialog := dialog.NewFileSave(saveFileCallbackFunction, window)
			newDialog.SetFileName(fileTorrentFilename)
			newDialog.Show()
		}))

	fileSizeTitle := getLabelCentered("File Size:")

	fileSizeLabel := getBoldLabelCentered(fileSizeDescription)

	fileExtractedSizeTitle := getLabelCentered("File Extracted Size:")

	fileExtractedSizeLabel := getBoldLabelCentered(extractedFileSizeDescription)

	verifyFileTitle := getBoldLabelCentered("Verify File")

	verifyFileDescription1 := getLabelCentered("You can use the Seekia client to verify your downloaded file.")
	verifyFileDescription2 := getLabelCentered("Press the button below and select your file.")
	verifyFileDescription3 := getLabelCentered("This will take a while, because the file contents must be hashed.")

	selectFileCallbackFunction := func(fyneFileObject fyne.URIReadCloser, err error){

		if (err != nil){
			setErrorEncounteredPage(window, err, currentPage)
			return
		}

		if (fyneFileObject == nil){
			return
		}

		setLoadingScreen(window, "Hashing File", "Calculating file hash...")

		filePath := fyneFileObject.URI().String()
		filePath = strings.TrimPrefix(filePath, "file://")

		fileObject, err := os.Open(filePath)
		if (err != nil){
			setErrorEncounteredPage(window, err, currentPage)
			return
		}
		defer fileObject.Close()

		hasher := zeeboBlake3.New()
		_, err = io.Copy(hasher, fileObject)
		if (err != nil){
			setErrorEncounteredPage(window, err, currentPage)
			return
		}

		hashResultBytes := hasher.Sum(nil)

		expectedResultBytes, err := encoding.DecodeHexStringToBytes(fileHashBlake3)
		if (err != nil){
			setErrorEncounteredPage(window, err, currentPage)
			return
		}

		currentPage()

		bytesAreEqual := bytes.Equal(hashResultBytes, expectedResultBytes)
		if (bytesAreEqual == false){
			title := "File Is Invalid"
			dialogMessage1 := getLabelCentered("The file you downloaded is not valid.")
			dialogMessage2 := getLabelCentered("The file's checksum does not match the expected checksum.")
			dialogContent := container.NewVBox(dialogMessage1, dialogMessage2)
			dialog.ShowCustom(title, "Close", dialogContent, window)
		} else {
			title := "File Is Valid"
			dialogMessage1 := getLabelCentered("The file you downloaded is valid!")
			dialogMessage2 := getLabelCentered("The file's checksum matches the expected checksum.")
			dialogContent := container.NewVBox(dialogMessage1, dialogMessage2)
			dialog.ShowCustom(title, "Close", dialogContent, window)
		}
	}

	verifyFileButton := getWidgetCentered(widget.NewButton("Verify File", func(){
			dialog.ShowFileOpen(selectFileCallbackFunction, window)
		}))

	page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), fileSelectorCentered, widget.NewSeparator(), fileNameTitle, fileNameLabel, widget.NewSeparator(), fileHashTitle, fileHashLabel, widget.NewSeparator(), torrentFileTitle, fileTorrentFilenameLabel, saveTorrentFileButton, widget.NewSeparator(), fileSizeTitle, fileSizeLabel, widget.NewSeparator(), fileExtractedSizeTitle, fileExtractedSizeLabel, widget.NewSeparator(), verifyFileTitle, verifyFileDescription1, verifyFileDescription2, verifyFileDescription3, verifyFileButton)

	scrollablePage := container.NewVScroll(page)

	window.SetContent(scrollablePage)
}


func setExtractDatasetsPage(window fyne.Window, fileIndex int, previousPage func()){

	if (fileIndex != 1 && fileIndex != 2){
		setErrorEncounteredPage(window, errors.New("setExtractDatasetsPage called with invalid fileIndex."), previousPage)
		return
	}

	currentPage := func(){setExtractDatasetsPage(window, fileIndex, previousPage)}

	title := getBoldLabelCentered("Extract Datasets")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("You must extract the downloaded genetic data archive files to folders.")
	description2 := getLabelCentered("Each data archive must be extracted into its own folder.")
	description3 := getLabelCentered("Once you have extracted the file, select the extracted folder using the selector below.")

	fileSelector := widget.NewSelect([]string{"File 1", "File 2"}, func(newSelection string){
			if (newSelection == "File 1"){
				if (fileIndex == 1){
					return
				}
				setExtractDatasetsPage(window, 1, previousPage)
			} else if (newSelection == "File 2"){
				if (fileIndex == 2){
					return
				}
				setExtractDatasetsPage(window, 2, previousPage)
			}
		})

	if (fileIndex == 1){
		fileSelector.Selected = "File 1"
	} else if (fileIndex == 2){
		fileSelector.Selected = "File 2"
	}

	fileSelectorCentered := getWidgetCentered(fileSelector)

	// Outputs:
	//	-string: File Name
	//	-string: Folderpath file name (The name of the file that stores the folderpath)
	//	-error
	getFileInfo := func()(string, string, error){
		if (fileIndex == 1){
			return "OpenSNPDataArchive1.tar.gz", "OpenSNPDataArchive1Folderpath.txt", nil
		}
		if (fileIndex == 2){
			return "OpenSNPDataArchive2.tar.gz", "OpenSNPDataArchive2Folderpath.txt", nil
		}

		return "", "", errors.New("setExtractDatasetsPage called with invalid fileIndex.")
	}

	fileName, fileFolderpathFilename, err := getFileInfo()
	if (err != nil){
		setErrorEncounteredPage(window, err, previousPage)
		return
	}

	fileNameTitle := getLabelCentered("File Name:")
	fileNameLabel := getBoldLabelCentered(fileName)

	currentLocationTitle := getLabelCentered("Current Folder Location:")

	getCurrentLocationLabel := func()(*fyne.Container, error){

		fileExists, fileContents, err := localFilesystem.GetFileContents("./" + fileFolderpathFilename)
		if (err != nil) { return nil, err }
		if (fileExists == false){
			noneLabel := getItalicLabelCentered("None")
			return noneLabel, nil
		}

		folderpathLabel := getBoldLabelCentered(string(fileContents))

		return folderpathLabel, nil
	}

	currentLocationLabel, err := getCurrentLocationLabel()
	if (err != nil) {
		setErrorEncounteredPage(window, err, previousPage)
		return
	}

	selectFolderCallbackFunction := func(folderObject fyne.ListableURI, err error){

		if (err != nil){
			title := "Failed to open folder."
			dialogMessage := getLabelCentered("Report this error to Seekia developers: " + err.Error())
			dialogContent := container.NewVBox(dialogMessage)
			dialog.ShowCustom(title, "Close", dialogContent, window)
			return
		}

		if (folderObject == nil) {
			return
		}

		folderPath := folderObject.Path()

		fileContents := []byte(folderPath)

		err = localFilesystem.CreateOrOverwriteFile(fileContents, "./", fileFolderpathFilename)
		if (err != nil){
			title := "Failed to save file."
			dialogMessage := getLabelCentered("Report this error to Seekia developers: " + err.Error())
			dialogContent := container.NewVBox(dialogMessage)
			dialog.ShowCustom(title, "Close", dialogContent, window)
			return
		}

		currentPage()
	}

	selectFolderLocationButton := getWidgetCentered(widget.NewButtonWithIcon("Select Folder Location", theme.FolderIcon(), func(){
			dialog.ShowFolderOpen(selectFolderCallbackFunction, window)
		}))

	page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), fileSelectorCentered, widget.NewSeparator(), fileNameTitle, fileNameLabel, widget.NewSeparator(), currentLocationTitle, currentLocationLabel, widget.NewSeparator(), selectFolderLocationButton)

	scrollablePage := container.NewVScroll(page)

	window.SetContent(scrollablePage)
}

func setExtractPersonAttributeDataPage(window fyne.Window, previousPage func()){

	currentPage := func(){setExtractPersonAttributeDataPage(window, previousPage)}

	title := getBoldLabelCentered("Extract Person Attribute Data")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("Press the button below to begin extracting the person attribute data from the datasets.")
	description2 := getLabelCentered("This will extract the phenotype and genetic information we want from each person's genome into files.")
	description3 := getLabelCentered("This will also construct our Genome-Tensor Conversion Guides.")  
	description4 := getLabelCentered("This will take a while.")

	beginExtractingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Extracting Data", theme.MediaPlayIcon(), func(){
			setStartAndMonitorExtractPersonAttributeDataPage(window, currentPage)
		}))

	page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, beginExtractingButton)

	window.SetContent(page)	
}

// We use this object to collect and display statistics about the dataset of users
type DatasetAttributePeopleStatistics struct{

	// This stores the quantity of people in the datasets that had any locus values for this attribute
	QuantityOfPeopleWithAnyLocusValues int

	// This stores the quantity of people in the datasets that had an attribute value for this attribute
	// Example: How many people actually had a height value.
	QuantityOfPeopleWithAttributeValue int

	// The quantity of attribute datas created for this attribute
	// Basically: Who had both any locus values for this attribute *and* an attribute value for this attribute
	QuantityOfAttributeDatasCreated int
}

// We use this object to collect and display statistics about the loci in the dataset
type DatasetAttributeLociStatistics struct{

	// This is the quantity of loci that were present in at least 1 person's genome
	QuantityOfLociKnownForAnyone int

	//TODO: Add quantityOfLociWithEnoughInstances so we can distinguish between
	//		loci which had enough instances, just not enough allele diversity

	// This is the quantity of loci which had at least 2 unique alleles present for enough people in the dataset
	QuantityOfLociWithEnoughAlleleDiversity int
}

func setStartAndMonitorExtractPersonAttributeDataPage(window fyne.Window, previousPage func()){

	err := locusMetadata.InitializeLocusMetadataVariables()
	if (err != nil){
		setErrorEncounteredPage(window, err, previousPage)
		return
	}

	title := getBoldLabelCentered("Extract Person Attribute Data")


	file1Exists, file1Contents, err := localFilesystem.GetFileContents("./OpenSNPDataArchive1Folderpath.txt")
	if (err != nil) {
		setErrorEncounteredPage(window, err, previousPage)
		return
	}
	if (file1Exists == false){

		backButton := getBackButtonCentered(previousPage)

		description1 := getBoldLabelCentered("You have not selected your OpenSNP data archive 1 folderpath.")
		description2 := getLabelCentered("Go back to step 2 and follow the instructions.")

		page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2)

		window.SetContent(page)
		return
	}

	dataArchive1Folderpath := string(file1Contents)


	file2Exists, file2Contents, err := localFilesystem.GetFileContents("./OpenSNPDataArchive2Folderpath.txt")
	if (err != nil) {
		setErrorEncounteredPage(window, err, previousPage)
		return
	}
	if (file2Exists == false){

		backButton := getBackButtonCentered(previousPage)

		description1 := getBoldLabelCentered("You have not selected your OpenSNP data archive 2 folderpath.")
		description2 := getLabelCentered("Go back to step 2 and follow the instructions.")

		page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2)

		window.SetContent(page)
		return
	}

	dataArchive2Folderpath := string(file2Contents)


	progressDetailsBinding := binding.NewString()
	estimatedTimeRemainingBinding := binding.NewString()
	progressPercentageBinding := binding.NewFloat()

	progressDetailsTitle := getBoldLabelCentered("Progress Details:")

	loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))

	progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding)
	progressDetailsLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel)

	estimatedTimeRemainingLabel := widget.NewLabelWithData(estimatedTimeRemainingBinding)
	estimatedTimeRemainingLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	estimatedTimeRemainingLabelCentered := getWidgetCentered(estimatedTimeRemainingLabel)

	// We set this bool to true to stop the extractData process
	var extractDataIsStoppedBoolMutex sync.RWMutex
	extractDataIsStoppedBool := false

	cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){

			extractDataIsStoppedBoolMutex.Lock()
			extractDataIsStoppedBool = true
			extractDataIsStoppedBoolMutex.Unlock()

			previousPage()
		}))

	page := container.NewVBox(title, widget.NewSeparator(), progressDetailsTitle, loadingBar, progressDetailsLabelCentered, estimatedTimeRemainingLabelCentered, widget.NewSeparator(), cancelButton)

	window.SetContent(page)

	extractPersonAttributeDataFunction := func(){

		var processProgressMutex sync.RWMutex
		// This stores the progress of extracting the data (0-1)
		processProgress := float64(0)

		startUpdateTimeRemainingDisplayFunction := func(){

			// This function updates the estimated time remaining label binding

			updateTimeRemainingDisplayFunction := func()error{

				startTime := time.Now().Unix()

				for{

					extractDataIsStoppedBoolMutex.RLock()
					extractDataIsStopped := extractDataIsStoppedBool
					extractDataIsStoppedBoolMutex.RUnlock()

					if (extractDataIsStopped == true){
						// User exited the process/Process has completed
						return nil
					}

					processProgressMutex.RLock()
					currentProcessProgress := processProgress
					processProgressMutex.RUnlock()

					if (currentProcessProgress == 0){
						estimatedTimeRemainingBinding.Set("Calculating required time...")
						time.Sleep(time.Second)
						continue
					}

					// We calculate how long we think it will take for the process to complete

					currentTime := time.Now().Unix()

					secondsElapsed := currentTime - startTime

					// processProgress is a float64 which stores the progress as a value between 0-1
					// To get the estimated total time the process will take, we divide the seconds elapsed by the proportion of progress
					// For example:
					// 0.1 (10%) at 10 seconds == Total process will take 100 seconds
					// 0.5 (50%) at 20 seconds == Total process will take 40 seconds

					totalSeconds := float64(secondsElapsed)/currentProcessProgress

					estimatedSecondsRemaining := int64(totalSeconds) - secondsElapsed

					estimatedTimeRemainingTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(estimatedSecondsRemaining, false)
					if (err != nil) { return err }

					estimatedTimeRemainingBinding.Set("Estimated Time Remaining: " + estimatedTimeRemainingTranslated)

					time.Sleep(time.Second)
				}

				// This cannot be reached
			}

			err := updateTimeRemainingDisplayFunction()
			if (err != nil){

				extractDataIsStoppedBoolMutex.Lock()
				extractDataIsStoppedBool = true
				extractDataIsStoppedBoolMutex.Unlock()

				fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
				return
			}
		}

		go startUpdateTimeRemainingDisplayFunction()

		//Outputs:
		//	-bool: Process completed (true == was not stopped mid-way)
		//	-bool: Data archive is well formed
		//	-map[string]DatasetAttributePeopleStatistics
		//		-Map Structure: Attribute Name -> DatasetAttributeStatistics
		//	-int: Total quantity of users with any genome information
		//	-map[string]DatasetAttributeLociStatistics
		//		-Map Structure: Attribute Name -> DatasetAttributeLociStatistics
		//	-error
		extractPersonAttributeDatas := func()(bool, bool, map[string]DatasetAttributePeopleStatistics, int, map[string]DatasetAttributeLociStatistics, error){

			err = progressDetailsBinding.Set("Reading user phenotype data and genome metadata...")
			if (err != nil) { return false, false, nil, 0, nil, err }

			phenotypesFilepath := goFilepath.Join(dataArchive2Folderpath, "OpenSNPData", "phenotypes_202504070100.csv")

			fileObject, err := os.Open(phenotypesFilepath)
			if (err != nil){

				fileDoesNotExist := os.IsNotExist(err)
				if (fileDoesNotExist == true){
					// Archive is corrupt
					return true, false, nil, 0, nil, nil
				}

				return false, false, nil, 0, nil, err
			}

			fileIsWellFormed, userPhenotypesList_OpenSNP := readBiobankData.ReadOpenSNPPhenotypesFile(fileObject)
			fileObject.Close()
			if (fileIsWellFormed == false){
				// Archive is corrupt
				return true, false, nil, 0, nil, nil
			}

			// Map Structure: User ID -> List of user raw genome filepaths
			userRawGenomeFilepathsMap := make(map[int][]string)

			// Outputs:
			//	-bool: Data archive is well formed
			//	-error
			addRawGenomesToMap := func(dataArchiveFolderpath string)(bool, error){

				// These are the folderpaths for the folders which contains all of the user raw genomes
				openSNPRawGenomesFolderpath := goFilepath.Join(dataArchiveFolderpath, "OpenSNPData")

				filesList, err := os.ReadDir(openSNPRawGenomesFolderpath)
				if (err != nil) { return false, err }

				for _, filesystemObject := range filesList{

					fileName := filesystemObject.Name()

					filepathIsFolder := filesystemObject.IsDir()
					if (filepathIsFolder == true){

						if (fileName == "picture_phenotypes_202504070100_all_pics"){
							continue
						}

						// Archive is corrupt

						return false, nil
					}

					fileInfoObject, err := filesystemObject.Info()
					if (err != nil) { return false, err }

					fileSize := fileInfoObject.Size()
					if (fileSize == 0){
						// The file is corrupted, we skip it.
						// These empty files were caused by a partially failed download.
						continue
					}

					// Example of a raw genome filename: "user1_file9_yearofbirth_1985_sex_XY.23andme"

					userIDWithRawGenomeInfo, fileIsUserGenome := strings.CutPrefix(fileName, "user")
					if (fileIsUserGenome == false){
						// File is not a user genome, skip it.
						continue
					}

					userIDString, rawGenomeInfo, separatorFound := strings.Cut(userIDWithRawGenomeInfo, "_")
					if (separatorFound == false){
						// Archive is corrupt
						return false, nil
					}

					userID, err := helpers.ConvertStringToInt(userIDString)
					if (err != nil){
						// Archive is corrupt
						return false, nil
					}

					getFileIsReadableStatus := func()bool{

						is23andMe := strings.HasSuffix(rawGenomeInfo, ".23andme.txt")
						if (is23andMe == true){
							// We can read this file
							return true
						}

						isAncestry := strings.HasSuffix(rawGenomeInfo, ".ancestry.txt")
						if (isAncestry == true){
							// We can read this file
							return true
						}

						// We cannot read this raw genome file
						//TODO: Add ability to read more raw genome files

						return false
					}

					fileIsReadable := getFileIsReadableStatus()
					if (fileIsReadable == true){

						rawGenomeFilepath := goFilepath.Join(openSNPRawGenomesFolderpath, fileName)

						existingList, exists := userRawGenomeFilepathsMap[userID]
						if (exists == false){
							userRawGenomeFilepathsMap[userID] = []string{rawGenomeFilepath}
						} else {

							// We first check to see if a genome file with the same name already exists in the list
							// This is to avoid duplicate files between both of the OpenSNP archives
							// All duplicates were attempted to be pruned from the second archive, so duplicate files shouldn't exist,
							//		but somehow there are a few...? I don't know why.
							// I checked all of them and the file contents are identical between the duplicates.

							checkIfDuplicateExists := func()bool{

								for _, existingFilepath := range existingList{

									existingFileName := goFilepath.Base(existingFilepath)

									if (fileName == existingFileName){
										return true
									}
								}

								return false
							}

							duplicateExists := checkIfDuplicateExists()
							if (duplicateExists == true){
								// We do not add the duplicate file to the filepaths list
								continue
							}

							existingList = append(existingList, rawGenomeFilepath)

							userRawGenomeFilepathsMap[userID] = existingList
						}
					}
				}

				return true, nil
			}

			archiveIsWellFormed, err := addRawGenomesToMap(dataArchive1Folderpath)
			if (err != nil){ return false, false, nil, 0, nil, err }
			if (archiveIsWellFormed == false){
				return true, false, nil, 0, nil, nil
			}

			archiveIsWellFormed, err = addRawGenomesToMap(dataArchive2Folderpath)
			if (err != nil){ return false, false, nil, 0, nil, err }
			if (archiveIsWellFormed == false){
				return true, false, nil, 0, nil, nil
			}

			// We create the folder to store the person attribute data

			_, err = localFilesystem.CreateFolder("./PersonAttributeDatas")
			if (err != nil) { return false, false, nil, 0, nil, err }

			// We create the folder to store the AttributeGenomeTensorConversionGuides

			_, err = localFilesystem.CreateFolder("./AttributeGenomeTensorConversionGuides")
			if (err != nil) { return false, false, nil, 0, nil, err }

			// We delete the contents of both folders

			folderExists, err := localFilesystem.DeleteAllFolderContents("./PersonAttributeDatas")
			if (err != nil) { return false, false, nil, 0, nil, err }
			if (folderExists == false){
				return false, false, nil, 0, nil, errors.New("CreateFolder failed to create folder.")
			}

			folderExists, err = localFilesystem.DeleteAllFolderContents("./AttributeGenomeTensorConversionGuides")
			if (err != nil) { return false, false, nil, 0, nil, err }
			if (folderExists == false){
				return false, false, nil, 0, nil, errors.New("CreateFolder failed to create folder.")
			}


			//TODO: Add more attributes
			attributeNamesList := []string{"Eye Color", "Lactose Tolerance", "Height", "Autism", "Homosexualness", "Obesity"}

			// We create the folders for each attribute's PersonAttributeData objects

			for _, attributeName := range attributeNamesList{

				attributeNameWithoutWhitespace := strings.ReplaceAll(attributeName, " ", "")

				folderpath := goFilepath.Join("./PersonAttributeDatas/", attributeNameWithoutWhitespace)

				_, err = localFilesystem.CreateFolder(folderpath)
				if (err != nil) { return false, false, nil, 0, nil, err }
			}

			// Now we create the allLociAllelesMap
			// This keeps track of the count of every allele in the training set
			// We use this to build the AttributeGenomeTensorConversionGuides, which convert a user's alleles into a neural network tensor
			// We keep track of this information because we only want neural network neurons for
			//		allele values which are common enough in the training data.
			// For example, if everyone only has G;G and G;T, then we don't need to consider all 33 other possible combinations

			type LocusAlleleInfo struct{

				RSID int64

				// Examples: "G","T"
				Allele string
			}

			// Map Structure: LocusAlleleInfo -> Quantity of people with this locus value at that rsID
			allLociAllelesMap := make(map[LocusAlleleInfo]int)

			numberOfUserPhenotypeDataObjects := len(userPhenotypesList_OpenSNP)
			maximumIndex := numberOfUserPhenotypeDataObjects-1

			numberOfUsersString := helpers.ConvertIntToString(numberOfUserPhenotypeDataObjects)

			// Map Structure: AttributeName -> DatasetAttributePeopleStatistics
			newDatasetAttributePeopleStatisticsMap := make(map[string]DatasetAttributePeopleStatistics)

			// This stores a count of users who have any valid genome locations
			totalQuantityOfUsersWithValidGenomes := 0

			for userIndex, userPhenotypeDataObject := range userPhenotypesList_OpenSNP{

				extractDataIsStoppedBoolMutex.RLock()
				extractDataIsStopped := extractDataIsStoppedBool
				extractDataIsStoppedBoolMutex.RUnlock()

				if (extractDataIsStopped == true){
					// User exited the process
					return false, false, nil, 0, nil, nil
				}

				userIndexString := helpers.ConvertIntToString(userIndex + 1)

				progressDetailsStatus := "Processing User " + userIndexString + "/" + numberOfUsersString

				err = progressDetailsBinding.Set(progressDetailsStatus)
				if (err != nil) { return false, false, nil, 0, nil, err }

				extractionProgressPercentage, err := helpers.ScaleIntProportionally(true, userIndex, 0, maximumIndex, 0, 100)
				if (err != nil) { return false, false, nil, 0, nil, err }

				extractionProgressFloat64 := float64(extractionProgressPercentage)/100

				err = progressPercentageBinding.Set(extractionProgressFloat64)
				if (err != nil) { return false, false, nil, 0, nil, err }

				processProgressMutex.Lock()
				processProgress = extractionProgressFloat64
				processProgressMutex.Unlock()

				userID := userPhenotypeDataObject.UserID

				userRawGenomeFilepathsList, exists := userRawGenomeFilepathsMap[userID]
				if (exists == false){
					// User has no genomes
					continue
				}

				// We read all of the user's raw genomes and combine them into a single genomeMap which excludes conflicting loci values

				userRawGenomesWithMetadataList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0)

				for _, userRawGenomeFilepath := range userRawGenomeFilepathsList{

					//Outputs:
					//	-bool: Able to read raw genome file
					//	-bool: Genome is phased
					//	-map[int64]readRawGenomes.RawGenomeLocusValue
					//	-error
					readRawGenomeMap := func()(bool, bool, map[int64]readRawGenomes.RawGenomeLocusValue, error){

						fileObject, err := os.Open(userRawGenomeFilepath)
						if (err != nil) { return false, false, nil, err }
						defer fileObject.Close()

						_, _, _, _, genomeIsPhased, rawGenomeMap, err := readRawGenomes.ReadRawGenomeFile(fileObject)
						if (err != nil) {
							//log.Println("Raw genome file is malformed: " + userRawGenomeFilepath + ". Reason: " + err.Error())
							return false, false, nil, nil
						}

						return true, genomeIsPhased, rawGenomeMap, nil
					}

					ableToReadRawGenome, rawGenomeIsPhased, rawGenomeMap, err := readRawGenomeMap()
					if (err != nil){ return false, false, nil, 0, nil, err }
					if (ableToReadRawGenome == false){
						// We cannot read this genome file
						// Many of the genome files are unreadable.
						//TODO: Improve ability to read slightly corrupted genome files
						continue
					}

					newGenomeIdentifier, err := helpers.GetNewRandom16ByteArray()
					if (err != nil) { return false, false, nil, 0, nil, err }

					rawGenomeWithMetadata := prepareRawGenomes.RawGenomeWithMetadata{
						GenomeIdentifier: newGenomeIdentifier,
						GenomeIsPhased: rawGenomeIsPhased,
						RawGenomeMap: rawGenomeMap,
					}

					userRawGenomesWithMetadataList = append(userRawGenomesWithMetadataList, rawGenomeWithMetadata)
				}

				if (len(userRawGenomesWithMetadataList) == 0){
					// None of the user's genome files are readable
					continue
				}

				totalQuantityOfUsersWithValidGenomes += 1

				//Outputs:
				//	-bool: Any useful locations exist in any of the user's genomes
				//	-map[int64]locusValue.LocusValue
				//	-error
				getUserLociValuesMap := func()(bool, map[int64]locusValue.LocusValue, error){

					updatePercentageCompleteFunction := func(_ int)error{
						return nil
					}

					anyUsefulLocationsExist, genomesWithMetadataList, _, combinedGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(userRawGenomesWithMetadataList, updatePercentageCompleteFunction)
					if (err != nil) { return false, nil, err }
					if (anyUsefulLocationsExist == false){
						// None of the user's genomes have any useful locations
						return false, nil, nil
					}
					if (combinedGenomesExist == false){

						if (len(genomesWithMetadataList) != 1){
							return false, nil, errors.New("GetGenomesWithMetadataListFromRawGenomesList returning non-1 length genomesWithMetadataList when combinedGenomesExist == false")
						}

						// Only 1 genome exists

						genomeWithMetadataObject := genomesWithMetadataList[0]

						genomeMap := genomeWithMetadataObject.GenomeMap

						return true, genomeMap, nil
					}

					for _, genomeWithMetadataObject := range genomesWithMetadataList{

						genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier

						if (genomeIdentifier == onlyExcludeConflictsGenomeIdentifier){

							genomeMap := genomeWithMetadataObject.GenomeMap

							return true, genomeMap, nil
						}
					}

					return false, nil, errors.New("OnlyExcludeConflicts genome not found from GetGenomesWithMetadataListFromRawGenomesList's returned list.")
				}

				anyUsefulLocationsExist, userLociValuesMap, err := getUserLociValuesMap()
				if (err != nil) { return false, false, nil, 0, nil, err }
				if (anyUsefulLocationsExist == false){
					// None of the user's genome files contain any useful locations
					continue
				}

				// Now we add info to the lociAllelesMap

				for rsID, userLocusValue := range userLociValuesMap{

					userAllele1 := userLocusValue.Base1Value
					userAllele2 := userLocusValue.Base2Value

					newLocusAllele1Info := LocusAlleleInfo{
						RSID: rsID,
						Allele: userAllele1,
					}

					allLociAllelesMap[newLocusAllele1Info] += 1

					if (userAllele1 == userAllele2){
						// We don't add the allele to the map twice
						// This is because we want to keep track of how many unique people possess each allele
						// For example, if 10 people have G;G, and nobody else has a G, we wouldn't count that as 20 instances,
						//		because we only care about how many unique people have the allele
						continue
					}

					newLocusAllele2Info := LocusAlleleInfo{
						RSID: rsID,
						Allele: userAllele2,
					}

					allLociAllelesMap[newLocusAllele2Info] += 1
				}

				// Now we create and save a PersonAttributeData for each attribute

				for _, attributeName := range attributeNamesList{

					getCurrentDatasetAttributePeopleStatisticsObject := func()DatasetAttributePeopleStatistics{

						attributeStatisticsObject, exists := newDatasetAttributePeopleStatisticsMap[attributeName]
						if (exists == false){
							newDatasetAttributeStatisticsObject := DatasetAttributePeopleStatistics{}
							return newDatasetAttributeStatisticsObject
						}

						return attributeStatisticsObject
					}

					currentDatasetAttributePeopleStatisticsObject := getCurrentDatasetAttributePeopleStatisticsObject()

					attributeLociList, err := geneticPrediction.GetAttributeLociList(attributeName)
					if (err != nil) { return false, false, nil, 0, nil, err }

					if (len(attributeLociList) == 0){
						return false, false, nil, 0, nil, errors.New("geneticPrediction.GetAttributeLociList returning empty attributeLociList.")
					}

					// We store the person's locus values for this attribute
					// This should be a subset of their entire locusValuesMap
					// Many of these loci may not actually be used by the neural network, if they are rare enough in the training dataset
					personAttributeLocusValuesMap := make(map[int64]locusValue.LocusValue)

					for _, rsID := range attributeLociList{

						personLocusValue, exists := userLociValuesMap[rsID]
						if (exists == true){
							personAttributeLocusValuesMap[rsID] = personLocusValue
						}
					}

					personAttributeValueExists, attributeIsNumeric, personAttributeValueDiscrete, personAttributeValueNumeric, err := geneticPrediction.GetPersonAttributeValueFromPhenotypeDataObject(attributeName, userPhenotypeDataObject)
					if (err != nil) { return false, false, nil, 0, nil, err }

					if (len(personAttributeLocusValuesMap) > 0){
						currentDatasetAttributePeopleStatisticsObject.QuantityOfPeopleWithAnyLocusValues += 1
					}
					if (personAttributeValueExists == true){
						currentDatasetAttributePeopleStatisticsObject.QuantityOfPeopleWithAttributeValue += 1
					}
					if (len(personAttributeLocusValuesMap) == 0 || personAttributeValueExists == false){
						// Person cannot be used to train the model.
						// They do not have any locus values and/or an attribute value for this attribute.
						newDatasetAttributePeopleStatisticsMap[attributeName] = currentDatasetAttributePeopleStatisticsObject
						continue
					}

					currentDatasetAttributePeopleStatisticsObject.QuantityOfAttributeDatasCreated += 1
					newDatasetAttributePeopleStatisticsMap[attributeName] = currentDatasetAttributePeopleStatisticsObject

					// We create a new personAttributeData and save it as a file

					newPersonAttributeData := geneticPrediction.PersonAttributeData{
						AttributeName: attributeName,
						LocusValuesMap: personAttributeLocusValuesMap,
						AttributeIsNumeric: attributeIsNumeric,
						AttributeValue_Discrete: personAttributeValueDiscrete,
						AttributeValue_Numeric: personAttributeValueNumeric,
					}

					// Now we save the user's PersonAttributeData to the PersonAttributeDatas subfolder.

					personAttributeDataBytes, err := geneticPrediction.EncodePersonAttributeDataObjectToBytes(newPersonAttributeData)
					if (err != nil) { return false, false, nil, 0, nil, err }

					attributeNameWithoutWhitespace := strings.ReplaceAll(attributeName, " ", "")

					personAttributeDatasAttributeFolderpath := goFilepath.Join("./PersonAttributeDatas", attributeNameWithoutWhitespace)

					userIDString := helpers.ConvertIntToString(userID)

					attributeDataFilename := "Person" + userIDString + "_AttributeData" + ".gob"

					err = localFilesystem.CreateOrOverwriteFile(personAttributeDataBytes, personAttributeDatasAttributeFolderpath, attributeDataFilename)
					if (err != nil) { return false, false, nil, 0, nil, err }
				}
			}

			// Now we create the genome tensor conversion guides for each attribute

			err = progressDetailsBinding.Set("Creating Genome-Tensor Conversion Guides...")
			if (err != nil) { return false, false, nil, 0, nil, err }

			// Map Structure: Attribute Name -> DatasetAttributeLociStatistics
			newDatasetAttributeLociStatisticsMap := make(map[string]DatasetAttributeLociStatistics)

			for _, attributeName := range attributeNamesList{

				attributeLociList, err := geneticPrediction.GetAttributeLociList(attributeName)
				if (err != nil) { return false, false, nil, 0, nil, err }

				attributeLociListCopy := slices.Clone(attributeLociList)

				newDatasetAttributeLociStatisticsObject := DatasetAttributeLociStatistics{}

				// We sort the list
				slices.Sort(attributeLociListCopy)

				// Map Structure: The locus rsID -> GenomeLocusTensorIndexesInfo
				newGenomeLocusTensorIndexesMap := make(map[int64]geneticPrediction.GenomeLocusTensorIndexesInfo)

				neuronIndex := 0

				for _, rsID := range attributeLociListCopy{

					// This keeps track of every known allele for either base (1 or 2)
					allKnownAllelesList := make([]string, 0)

					anyPersonHadKnownValue := false

					for locusInfo, locusValueCount := range allLociAllelesMap{

						locusRSID := locusInfo.RSID
						if (locusRSID != rsID){
							continue
						}

						anyPersonHadKnownValue = true

						if (locusValueCount < 10){
							// There were fewer than 10 people in the training set who had this locus allele value
							// It's not possible to use it to train the neural network.
							continue
						}

						locusAllele := locusInfo.Allele

						allKnownAllelesList = append(allKnownAllelesList, locusAllele)
					}

					if (anyPersonHadKnownValue == false){
						continue
					}

					newDatasetAttributeLociStatisticsObject.QuantityOfLociKnownForAnyone += 1

					if (len(allKnownAllelesList) < 2){
						// There aren't at least 2 possible alleles with enough examples for this locus in our training set
						// Without at least 2 states, training is impossible 
						continue
					}

					newDatasetAttributeLociStatisticsObject.QuantityOfLociWithEnoughAlleleDiversity += 1

					containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(allKnownAllelesList)
					if (containsDuplicates == true){
						// This should never happen
						return false, false, nil, 0, nil, errors.New("allKnownAllelesList contains duplicates")
					}

					// We have a list of every possible allele
					// 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

					locusExistsIndex := neuronIndex

					neuronIndex += 1

					LocusIsPhasedIndex := neuronIndex

					neuronIndex += 1

					slices.Sort(allKnownAllelesList)

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

					for _, alleleValue := range allKnownAllelesList{

						newAllele1IndexesMap[alleleValue] = neuronIndex

						neuronIndex += 1
					}

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

					for _, alleleValue := range allKnownAllelesList{

						newAllele2IndexesMap[alleleValue] = neuronIndex

						neuronIndex += 1
					}

					newGenomeLocusTensorIndexesInfo := geneticPrediction.GenomeLocusTensorIndexesInfo{
						LocusExistsIndex: locusExistsIndex,
						LocusIsPhasedIndex: LocusIsPhasedIndex,
						Allele1IndexesMap: newAllele1IndexesMap,
						Allele2IndexesMap: newAllele2IndexesMap,
					}

					newGenomeLocusTensorIndexesMap[rsID] = newGenomeLocusTensorIndexesInfo
				}

				newDatasetAttributeLociStatisticsMap[attributeName] = newDatasetAttributeLociStatisticsObject

				if (neuronIndex == 0){
					// None of the loci for this attribute had enough people with a value
					return false, false, nil, 0, nil, errors.New("Not enough training data exists for attribute: " + attributeName)
				}

				totalNeuronCount := neuronIndex + 1

				newGenomeTensorConversionGuide := geneticPrediction.AttributeGenomeTensorConversionGuide{
					QuantityOfInputNeurons: totalNeuronCount,
					GenomeLocusTensorIndexesMap: newGenomeLocusTensorIndexesMap,
				}

				newAttributeGenomeTensorConversionGuideBytes, err := geneticPrediction.EncodeAttributeGenomeTensorConversionGuideObjectToBytes(newGenomeTensorConversionGuide)
				if (err != nil) { return false, false, nil, 0, nil, err }

				// We save the conversion guide to a file

				attributeNameWithoutWhitespace := strings.ReplaceAll(attributeName, " ", "")

				conversionGuidesFolderpath := "./AttributeGenomeTensorConversionGuides"

				conversionGuideFilename := attributeNameWithoutWhitespace + "GenomeTensorConversionGuide.gob"

				err = localFilesystem.CreateOrOverwriteFile(newAttributeGenomeTensorConversionGuideBytes, conversionGuidesFolderpath, conversionGuideFilename)
				if (err != nil) { return false, false, nil, 0, nil, err }
			}

			extractDataIsStoppedBoolMutex.Lock()
			extractDataIsStoppedBool = true
			extractDataIsStoppedBoolMutex.Unlock()

			return true, true, newDatasetAttributePeopleStatisticsMap, totalQuantityOfUsersWithValidGenomes, newDatasetAttributeLociStatisticsMap, nil
		}

		processIsComplete, archiveIsWellFormed, newDatasetAttributePeopleStatisticsMap, totalQuantityOfUsersWithValidGenomes, datasetAttributeLociStatisticsMap, err := extractPersonAttributeDatas()
		if (err != nil){
			extractDataIsStoppedBoolMutex.Lock()
			extractDataIsStoppedBool = true
			extractDataIsStoppedBoolMutex.Unlock()

			fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
			return
		}
		if (processIsComplete == false){
			// User exited the page
			return
		}
		if (archiveIsWellFormed == false){

			title := getBoldLabelCentered("OpenSNP Archive Is Corrupt")

			description1 := getBoldLabelCentered("Your downloaded OpenSNP data archive is corrupt.")
			description2 := getLabelCentered("The extracted folder contents do not match what the archive should contain.")
			description3 := getLabelCentered("You should re-extract the contents of the archive.")

			exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), previousPage))

			page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, exitButton)

			fyne.Do(func(){window.SetContent(page)})
			return
		}

		fyne.Do(func(){setExtractPersonAttributeDataIsCompletePage(window, newDatasetAttributePeopleStatisticsMap, totalQuantityOfUsersWithValidGenomes, datasetAttributeLociStatisticsMap)})
	}

	go extractPersonAttributeDataFunction()
}


func setExtractPersonAttributeDataIsCompletePage(window fyne.Window, datasetAttributePeopleStatisticsMap map[string]DatasetAttributePeopleStatistics, totalQuantityOfUsersWithValidGenomes int, datasetAttributeLociStatisticsMap map[string]DatasetAttributeLociStatistics){

	title := getBoldLabelCentered("Extracting Person Attribute Data Is Complete")

	description1 := getLabelCentered("Extracting person attribute data is complete!")
	description2 := getLabelCentered("The data have been saved in the PersonAttributeDatas and AttributeGenomeTensorConversionGuides folders.")
	description3 := getLabelCentered("Below is a table describing statistics about the dataset.")

	exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), func(){
			setHomePage(window)
		}))

	getDatasetPeopleStatisticsGrid := func()(*fyne.Container, error){

		emptyLabel1 := widget.NewLabel("")
		attributeNameTitle := getItalicLabelCentered("Attribute Name:")

		hadAnyTitle := getItalicLabelCentered("Had Any")
		locusValuesTitle := getItalicLabelCentered("Locus Values")

		hadAnTitle := getItalicLabelCentered("Had An")
		attributeValueTitle := getItalicLabelCentered("Attribute Value")

		emptyLabel2 := widget.NewLabel("")
		hadBothTitle := getItalicLabelCentered("Had Both")

		attributeNameColumn := container.NewVBox(emptyLabel1, attributeNameTitle, widget.NewSeparator())
		hadAnyLocusValuesColumn := container.NewVBox(hadAnyTitle, locusValuesTitle, widget.NewSeparator())
		hadAnAttributeValueColumn := container.NewVBox(hadAnTitle, attributeValueTitle, widget.NewSeparator())
		hadBothColumn := container.NewVBox(emptyLabel2, hadBothTitle, widget.NewSeparator())

		attributeNamesList := helpers.GetListOfMapKeys(datasetAttributePeopleStatisticsMap)

		slices.Sort(attributeNamesList)

		for _, attributeName := range attributeNamesList{

			datasetAttributePeopleStatisticsObject, exists := datasetAttributePeopleStatisticsMap[attributeName]
			if (exists == false){
				return nil, errors.New("datasetAttributePeopleStatisticsMap missing entry for attributeName.")
			}

			quantityOfPeopleWithAnyLocusValues := datasetAttributePeopleStatisticsObject.QuantityOfPeopleWithAnyLocusValues
			quantityOfPeopleWithAttributeValue := datasetAttributePeopleStatisticsObject.QuantityOfPeopleWithAttributeValue
			quantityOfAttributeDatasCreated := datasetAttributePeopleStatisticsObject.QuantityOfAttributeDatasCreated			

			percentageOfPeopleWithAnyLocusValues := (float64(quantityOfPeopleWithAnyLocusValues)/float64(totalQuantityOfUsersWithValidGenomes))*100

			percentageOfPeopleWithAttributeValue := (float64(quantityOfPeopleWithAttributeValue)/float64(totalQuantityOfUsersWithValidGenomes))*100

			percentageOfPeopleWhoHadBoth := (float64(quantityOfAttributeDatasCreated)/float64(totalQuantityOfUsersWithValidGenomes))*100

			quantityOfPeopleWithAnyLocusValuesString := helpers.ConvertIntToString(quantityOfPeopleWithAnyLocusValues)
			percentageOfPeopleWithAnyLocusValuesString := helpers.ConvertFloat64ToStringRounded(percentageOfPeopleWithAnyLocusValues, 0)

			quantityOfPeopleWithAttributeValueString := helpers.ConvertIntToString(quantityOfPeopleWithAttributeValue)
			quantityOfAttributeDatasCreatedString := helpers.ConvertIntToString(quantityOfAttributeDatasCreated)

			percentageOfPeopleWithAttributeValueString := helpers.ConvertFloat64ToStringRounded(percentageOfPeopleWithAttributeValue, 0)
			percentageOfPeopleWhoHadBothString := helpers.ConvertFloat64ToStringRounded(percentageOfPeopleWhoHadBoth, 0)

			totalQuantityOfUsersWithValidGenomesString := helpers.ConvertIntToString(totalQuantityOfUsersWithValidGenomes)

			hadAnyLocusValuesLabelText := quantityOfPeopleWithAnyLocusValuesString + "/" + totalQuantityOfUsersWithValidGenomesString + " (" + percentageOfPeopleWithAnyLocusValuesString + "%)"

			hadAnAttributeValueLabelText := quantityOfPeopleWithAttributeValueString + "/" + totalQuantityOfUsersWithValidGenomesString + " (" + percentageOfPeopleWithAttributeValueString + "%)"

			hadBothLabelText := quantityOfAttributeDatasCreatedString + "/" + totalQuantityOfUsersWithValidGenomesString + " (" + percentageOfPeopleWhoHadBothString + "%)"

			attributeNameLabel := getBoldLabelCentered(attributeName)
			hadAnyLocusValuesLabel := getBoldLabelCentered(hadAnyLocusValuesLabelText)
			hadAnAttributeValueLabel := getBoldLabelCentered(hadAnAttributeValueLabelText)
			hadBothLabel := getBoldLabelCentered(hadBothLabelText)

			attributeNameColumn.Add(attributeNameLabel)
			hadAnyLocusValuesColumn.Add(hadAnyLocusValuesLabel)
			hadAnAttributeValueColumn.Add(hadAnAttributeValueLabel)
			hadBothColumn.Add(hadBothLabel)

			attributeNameColumn.Add(widget.NewSeparator())
			hadAnyLocusValuesColumn.Add(widget.NewSeparator())
			hadAnAttributeValueColumn.Add(widget.NewSeparator())
			hadBothColumn.Add(widget.NewSeparator())
		}

		gridContainer := container.NewHBox(layout.NewSpacer(), attributeNameColumn, hadAnyLocusValuesColumn, hadAnAttributeValueColumn, hadBothColumn, layout.NewSpacer())

		return gridContainer, nil
	}

	datasetPeopleStatisticsGrid, err := getDatasetPeopleStatisticsGrid()
	if (err != nil){
		setErrorEncounteredPage(window, err, func(){setHomePage(window)})
		return
	}

	getDatasetLociStatisticsGrid := func()(*fyne.Container, error){

		emptyLabel1 := widget.NewLabel("")
		attributeNameTitle := getItalicLabelCentered("Attribute Name:")

		quantityOfLociTitle1 := getItalicLabelCentered("Quantity Of Loci")
		knownForAnyoneTitle := getItalicLabelCentered("Known For Anyone")

		quantityOfLociTitle2 := getItalicLabelCentered("Quantity Of Loci")
		withEnoughAlleleDiversityTitle := getItalicLabelCentered("With Enough Allele Diversity")

		attributeNameColumn := container.NewVBox(emptyLabel1, attributeNameTitle, widget.NewSeparator())
		quantityOfLociKnownForAnyoneColumn := container.NewVBox(quantityOfLociTitle1, knownForAnyoneTitle, widget.NewSeparator())
		quantityOfLociWithEnoughAlleleDiversityColumn := container.NewVBox(quantityOfLociTitle2, withEnoughAlleleDiversityTitle, widget.NewSeparator())

		attributeNamesList := helpers.GetListOfMapKeys(datasetAttributeLociStatisticsMap)

		slices.Sort(attributeNamesList)

		for _, attributeName := range attributeNamesList{

			attributeLociList, err := geneticPrediction.GetAttributeLociList(attributeName)
			if (err != nil) { return nil, err }

			totalQuantityOfLoci := len(attributeLociList)

			datasetAttributeLociStatisticsObject, exists := datasetAttributeLociStatisticsMap[attributeName]
			if (exists == false){
				return nil, errors.New("datasetAttributeLociStatisticsMap missing entry for attributeName.")
			}

			quantityOfLociKnownForAnyone := datasetAttributeLociStatisticsObject.QuantityOfLociKnownForAnyone
			quantityOfLociWithEnoughAlleleDiversity := datasetAttributeLociStatisticsObject.QuantityOfLociWithEnoughAlleleDiversity

			percentageOfLociKnownForAnyone := (float64(quantityOfLociKnownForAnyone)/float64(totalQuantityOfLoci))*100
			percentageOfLociWithEnoughAlleleDiversity := (float64(quantityOfLociWithEnoughAlleleDiversity)/float64(totalQuantityOfLoci))*100

			quantityOfLociKnownForAnyoneString := helpers.ConvertIntToString(quantityOfLociKnownForAnyone)
			quantityOfLociWithEnoughAlleleDiversityString := helpers.ConvertIntToString(quantityOfLociWithEnoughAlleleDiversity)

			percentageOfLociKnownForAnyoneString := helpers.ConvertFloat64ToStringRounded(percentageOfLociKnownForAnyone, 0)
			percentageOfLociWithEnoughAlleleDiversityString := helpers.ConvertFloat64ToStringRounded(percentageOfLociWithEnoughAlleleDiversity, 0)

			totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci)

			lociKnownForAnyoneLabelText := quantityOfLociKnownForAnyoneString + "/" + totalQuantityOfLociString + " (" + percentageOfLociKnownForAnyoneString + "%)"

			lociWithEnoughAlleleDiversityLabelText := quantityOfLociWithEnoughAlleleDiversityString + "/" + totalQuantityOfLociString + " (" + percentageOfLociWithEnoughAlleleDiversityString + "%)"

			attributeNameLabel := getBoldLabelCentered(attributeName)
			lociKnownForAnyoneLabel := getBoldLabelCentered(lociKnownForAnyoneLabelText)
			lociWithEnoughAlleleDiversityLabel := getBoldLabelCentered(lociWithEnoughAlleleDiversityLabelText)

			attributeNameColumn.Add(attributeNameLabel)
			quantityOfLociKnownForAnyoneColumn.Add(lociKnownForAnyoneLabel)
			quantityOfLociWithEnoughAlleleDiversityColumn.Add(lociWithEnoughAlleleDiversityLabel)

			attributeNameColumn.Add(widget.NewSeparator())
			quantityOfLociKnownForAnyoneColumn.Add(widget.NewSeparator())
			quantityOfLociWithEnoughAlleleDiversityColumn.Add(widget.NewSeparator())
		}

		gridContainer := container.NewHBox(layout.NewSpacer(), attributeNameColumn, quantityOfLociKnownForAnyoneColumn, quantityOfLociWithEnoughAlleleDiversityColumn, layout.NewSpacer())

		return gridContainer, nil
	}

	datasetLociStatisticsGrid, err := getDatasetLociStatisticsGrid()
	if (err != nil) {
		setErrorEncounteredPage(window, err, func(){setHomePage(window)})
		return
	}

	page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, exitButton, widget.NewSeparator(), datasetPeopleStatisticsGrid, widget.NewSeparator(), datasetLociStatisticsGrid)

	pageScrollable := container.NewVScroll(page)

	window.SetContent(pageScrollable)
}


func setCreateTrainingDataPage(window fyne.Window, previousPage func()){

	currentPage := func(){setCreateTrainingDataPage(window, previousPage)}

	title := getBoldLabelCentered("Create Training Data")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("Press the button below to begin creating the training data.")
	description2 := getLabelCentered("This will prepare each person's genome into a file to use to train each neural network.")
	description3 := getLabelCentered("This will take a while.")

	beginCreatingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Creating Data", theme.MediaPlayIcon(), func(){
			setStartAndMonitorCreateTrainingDataPage(window, currentPage)
		}))

	page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, beginCreatingButton)

	window.SetContent(page)
}



func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage func()){

	err := locusMetadata.InitializeLocusMetadataVariables()
	if (err != nil){
		setErrorEncounteredPage(window, err, previousPage)
		return
	}

	title := getBoldLabelCentered("Creating Training Data")

	progressDetailsBinding := binding.NewString()
	estimatedTimeRemainingBinding := binding.NewString()
	progressPercentageBinding := binding.NewFloat()

	progressDetailsTitle := getBoldLabelCentered("Progress Details:")

	loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))

	progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding)
	progressDetailsLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel)

	estimatedTimeRemainingLabel := widget.NewLabelWithData(estimatedTimeRemainingBinding)
	estimatedTimeRemainingLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	estimatedTimeRemainingLabelCentered := getWidgetCentered(estimatedTimeRemainingLabel)

	// We set this bool to true to stop the createData process
	var createDataIsStoppedBoolMutex sync.RWMutex
	createDataIsStoppedBool := false

	cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){

			createDataIsStoppedBoolMutex.Lock()
			createDataIsStoppedBool = true
			createDataIsStoppedBoolMutex.Unlock()

			previousPage()
		}))

	page := container.NewVBox(title, widget.NewSeparator(), progressDetailsTitle, loadingBar, progressDetailsLabelCentered, estimatedTimeRemainingLabelCentered, widget.NewSeparator(), cancelButton)

	window.SetContent(page)

	createTrainingDataFunction := func(){

		var processProgressMutex sync.RWMutex
		// This stores the progress of creating the training data (0-1)
		processProgress := float64(0)

		startUpdateTimeRemainingDisplayFunction := func(){

			// This function updates the estimated time remaining label binding

			updateTimeRemainingDisplayFunction := func()error{

				startTime := time.Now().Unix()

				for{

					createDataIsStoppedBoolMutex.RLock()
					createDataIsStopped := createDataIsStoppedBool
					createDataIsStoppedBoolMutex.RUnlock()

					if (createDataIsStopped == true){
						// User exited the process/Process has completed
						return nil
					}

					processProgressMutex.RLock()
					currentProcessProgress := processProgress
					processProgressMutex.RUnlock()

					if (currentProcessProgress == 0){
						estimatedTimeRemainingBinding.Set("Calculating required time...")
						time.Sleep(time.Second)
						continue
					}

					// We calculate how long we think it will take for the process to complete

					currentTime := time.Now().Unix()

					secondsElapsed := currentTime - startTime

					// processProgress is a float64 which stores the progress as a value between 0-1
					// To get the estimated total time the process will take, we divide the seconds elapsed by the proportion of progress
					// For example:
					// 0.1 (10%) at 10 seconds == Total process will take 100 seconds
					// 0.5 (50%) at 20 seconds == Total process will take 40 seconds

					totalSeconds := float64(secondsElapsed)/currentProcessProgress

					estimatedSecondsRemaining := int64(totalSeconds) - secondsElapsed

					estimatedTimeRemainingTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(estimatedSecondsRemaining, false)
					if (err != nil) { return err }

					estimatedTimeRemainingBinding.Set("Estimated Time Remaining: " + estimatedTimeRemainingTranslated)

					time.Sleep(time.Second)
				}

				// This cannot be reached
			}

			err := updateTimeRemainingDisplayFunction()
			if (err != nil){

				createDataIsStoppedBoolMutex.Lock()
				createDataIsStoppedBool = true
				createDataIsStoppedBoolMutex.Unlock()

				fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
				return
			}
		}

		go startUpdateTimeRemainingDisplayFunction()

		//Outputs:
		//	-bool: Process completed (true == was not stopped mid-way)
		//	-error
		createTrainingData := func()(bool, error){

			// We create the folder to store the training data

			err = progressDetailsBinding.Set("Initializing TrainingData Folder...")
			if (err != nil) { return false, err }

			err := localFilesystem.DeleteFileOrFolder("./TrainingData")
			if (err != nil) { return false, err }

			_, err = localFilesystem.CreateFolder("./TrainingData")
			if (err != nil) { return false, err }

			//TODO: Add more attributes
			attributeNamesList := []string{"Eye Color", "Lactose Tolerance", "Height", "Autism", "Homosexualness", "Obesity"}

			// We create the folders for each attribute's training data

			err = progressDetailsBinding.Set("Counting Attribute Datas...")
			if (err != nil) { return false, err }

			// We count up how many PersonAttributeDatas exist so we can keep track of progress
			totalPersonAttributeDatasCount := 0

			for _, attributeName := range attributeNamesList{

				attributeNameWithoutWhitespace := strings.ReplaceAll(attributeName, " ", "")

				trainingDataFolderpath := goFilepath.Join("./TrainingData/", attributeNameWithoutWhitespace)

				_, err = localFilesystem.CreateFolder(trainingDataFolderpath)
				if (err != nil) { return false, err }


				attributePersonAttributeDatasFolderpath := goFilepath.Join("./PersonAttributeDatas", attributeNameWithoutWhitespace)

				folderExists, quantityOfFiles, err := localFilesystem.GetQuantityOfFilesInFolder(attributePersonAttributeDatasFolderpath)
				if (err != nil) { return false, err }
				if (folderExists == false){
					return false, errors.New("PersonAttributeData folder for attribute not found: " + attributePersonAttributeDatasFolderpath)
				}

				totalPersonAttributeDatasCount += quantityOfFiles
			}

			totalPersonAttributeDatasCountString := helpers.ConvertIntToString(totalPersonAttributeDatasCount)

			totalProcessedAttributeDatas := 0

			for _, attributeName := range attributeNamesList{

				createDataIsStoppedBoolMutex.RLock()
				createDataIsStopped := createDataIsStoppedBool
				createDataIsStoppedBoolMutex.RUnlock()

				if (createDataIsStopped == true){
					// User exited the process
					return false, nil
				}

				// We first retrieve the AttributeGenomeTensorConversionGuide

				attributeNameWithoutWhitespace := strings.ReplaceAll(attributeName, " ", "")

				attributeGenomeTensorConversionGuideFolderpath := "./AttributeGenomeTensorConversionGuides"
				attributeGenomeTensorConversionGuideFilename := attributeNameWithoutWhitespace + "GenomeTensorConversionGuide.gob"

				fileLocation := goFilepath.Join(attributeGenomeTensorConversionGuideFolderpath, attributeGenomeTensorConversionGuideFilename)

				fileExists, fileContents, err := localFilesystem.GetFileContents(fileLocation)
				if (err != nil) { return false, err }
				if (fileExists == false){
					return false, errors.New("Attribute genome tensor conversion guide file not found: " + fileLocation)
				}

				attributeGenomeTensorConversionGuide, err := geneticPrediction.DecodeBytesToAttributeGenomeTensorConversionGuideObject(fileContents)
				if (err != nil) { return false, err }

				attributePersonAttributeDatasFolderpath := goFilepath.Join("./PersonAttributeDatas", attributeNameWithoutWhitespace)
				trainingDataFolderpath := goFilepath.Join("./TrainingData", attributeNameWithoutWhitespace)

				folderExists, allPersonAttributeDataFilenames, err := localFilesystem.GetAllFilenamesInFolder(attributePersonAttributeDatasFolderpath)
				if (err != nil) { return false, err }
				if (folderExists == false){
					return false, errors.New("PersonAttributeDatas folder is not found for attribute: " + attributeName)
				}

				// We sort so we get the same order of people every time
				slices.Sort(allPersonAttributeDataFilenames)

				for personIndex, personAttributeDataFilename := range allPersonAttributeDataFilenames{

					createDataIsStoppedBoolMutex.RLock()
					createDataIsStopped := createDataIsStoppedBool
					createDataIsStoppedBoolMutex.RUnlock()

					if (createDataIsStopped == true){
						// User exited the process
						return false, nil
					}

					totalProcessedAttributeDatasString := helpers.ConvertIntToString(totalProcessedAttributeDatas)

					progressDetailsStatus := "Processing Attribute Data " + totalProcessedAttributeDatasString + "/" + totalPersonAttributeDatasCountString

					err = progressDetailsBinding.Set(progressDetailsStatus)
					if (err != nil) { return false, err }

					creationProgressPercentage, err := helpers.ScaleIntProportionally(true, totalProcessedAttributeDatas, 0, totalPersonAttributeDatasCount, 0, 100)
					if (err != nil) { return false, err }

					creationProgressFloat64 := float64(creationProgressPercentage)/100

					err = progressPercentageBinding.Set(creationProgressFloat64)
					if (err != nil) { return false, err }

					processProgressMutex.Lock()
					processProgress = creationProgressFloat64
					processProgressMutex.Unlock()

					personAttributeDataFilepath := goFilepath.Join(attributePersonAttributeDatasFolderpath, personAttributeDataFilename) 

					fileExists, fileContents, err := localFilesystem.GetFileContents(personAttributeDataFilepath)
					if (err != nil) { return false, err }
					if (fileExists == false){
						return false, errors.New("Person attribute data not found: " + personAttributeDataFilepath)
					}

					personAttributeData, err := geneticPrediction.DecodeBytesToPersonAttributeDataObject(fileContents)
					if (err != nil) { return false, err }

					anyDataCreated, personTrainingDataList, err := geneticPrediction.CreatePersonGeneticPredictionTrainingData(personIndex, personAttributeData, attributeGenomeTensorConversionGuide)
					if (err != nil) { return false, err }
					if (anyDataCreated == false){
						totalProcessedAttributeDatas += 1
						continue
					}

					personIndexString := helpers.ConvertIntToString(personIndex)

					for index, trainingData := range personTrainingDataList{

						personTrainingDataBytes, err := geneticPrediction.EncodeTrainingDataObjectToBytes(trainingData)
						if (err != nil) { return false, err }

						trainingDataIndexString := helpers.ConvertIntToString(index+1)

						trainingDataFilename := "Person" + personIndexString + "_TrainingData_" + trainingDataIndexString + ".gob"

						err = localFilesystem.CreateOrOverwriteFile(personTrainingDataBytes, trainingDataFolderpath, trainingDataFilename)
						if (err != nil) { return false, err }
					}

					totalProcessedAttributeDatas += 1
				}
			}

			createDataIsStoppedBoolMutex.Lock()
			createDataIsStoppedBool = true
			createDataIsStoppedBoolMutex.Unlock()

			return true, nil
		}

		processIsComplete, err := createTrainingData()
		if (err != nil){
			createDataIsStoppedBoolMutex.Lock()
			createDataIsStoppedBool = true
			createDataIsStoppedBoolMutex.Unlock()

			fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
			return
		}
		if (processIsComplete == false){
			// User exited the page
			return
		}

		fyne.Do(func(){setCreateTrainingDataIsCompletePage(window)})
	}

	go createTrainingDataFunction()
}

func setCreateTrainingDataIsCompletePage(window fyne.Window){

	title := getBoldLabelCentered("Creating Data Is Complete")

	description1 := getLabelCentered("Creating training data is complete!")
	description2 := getLabelCentered("The data have been saved in the TrainingData folder.")

	exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), func(){
			setHomePage(window)
		}))

	page := container.NewVBox(title, widget.NewSeparator(), description1, description2, exitButton)

	window.SetContent(page)
}


func setTrainModelsPage(window fyne.Window, previousPage func()){

	currentPage := func(){setTrainModelsPage(window, previousPage)}

	title := getBoldLabelCentered("Train Models")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("Press the button below to begin training a genetic model.")
	description2 := getLabelCentered("This will train a neural network using the user training data.")
	description3 := getLabelCentered("This will take a while.")
	description4 := getLabelCentered("You must select a model to train.")

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

	attributeNameSelector := widget.NewSelect(attributeNamesList, nil)

	beginTrainingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Training Model", theme.MediaPlayIcon(), func(){

			selectedAttributeIndex := attributeNameSelector.SelectedIndex()
			if (selectedAttributeIndex < 0){
				title := "No Attribute Selected"
				dialogMessage1 := getLabelCentered("You must select an attribute model to train.")
				dialogContent := container.NewVBox(dialogMessage1)
				dialog.ShowCustom(title, "Close", dialogContent, window)
				return
			}

			attributeName := attributeNameSelector.Selected
			setStartAndMonitorTrainModelPage(window, attributeName, currentPage)
		}))

	attributeNameSelectorCentered := getWidgetCentered(attributeNameSelector)

	page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), attributeNameSelectorCentered, widget.NewSeparator(), beginTrainingButton)

	window.SetContent(page)
}

func setStartAndMonitorTrainModelPage(window fyne.Window, attributeName string, previousPage func()){

	title := getBoldLabelCentered("Train Model")

	modelNameTitle := getLabelCentered("Model Name:")

	modelNameLabel := getBoldLabelCentered(attributeName)

	//TODO: Verify TrainingData folder integrity

	progressDetailsTitle := getBoldLabelCentered("Progress Details:")

	progressDetailsBinding := binding.NewString()
	estimatedTimeRemainingBinding := binding.NewString()
	progressPercentageBinding := binding.NewFloat()

	loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))

	progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding)
	progressDetailsLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel)

	estimatedTimeRemainingLabel := widget.NewLabelWithData(estimatedTimeRemainingBinding)
	estimatedTimeRemainingLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	estimatedTimeRemainingLabelCentered := getWidgetCentered(estimatedTimeRemainingLabel)

	// We set this bool to true to stop the trainModel process
	var trainModelIsStoppedBoolMutex sync.RWMutex
	trainModelIsStoppedBool := false

	cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){

			trainModelIsStoppedBoolMutex.Lock()
			trainModelIsStoppedBool = true
			trainModelIsStoppedBoolMutex.Unlock()

			previousPage()
		}))

	page := container.NewVBox(title, widget.NewSeparator(), modelNameTitle, modelNameLabel, widget.NewSeparator(), progressDetailsTitle, loadingBar, progressDetailsLabelCentered, estimatedTimeRemainingLabelCentered, widget.NewSeparator(), cancelButton)

	window.SetContent(page)

	trainModelFunction := func(){

		var processProgressMutex sync.RWMutex
		// This stores the amount of progress which has been completed (0-1)
		processProgress := float64(0)

		startUpdateTimeRemainingDisplayFunction := func(){

			// This function updates the estimated time remaining label binding

			updateTimeRemainingDisplayFunction := func()error{

				startTime := time.Now().Unix()

				for{

					trainModelIsStoppedBoolMutex.RLock()
					trainModelIsStopped := trainModelIsStoppedBool
					trainModelIsStoppedBoolMutex.RUnlock()

					if (trainModelIsStopped == true){
						// User exited the process/Process has completed
						return nil
					}

					processProgressMutex.RLock()
					currentProcessProgress := processProgress
					processProgressMutex.RUnlock()

					if (currentProcessProgress == 0){
						estimatedTimeRemainingBinding.Set("Calculating required time...")
						time.Sleep(time.Second)
						continue
					}

					// We calculate how long we think it will take for the process to complete

					currentTime := time.Now().Unix()

					secondsElapsed := currentTime - startTime

					// processProgress is a float64 which stores the progress as a value between 0-1
					// To get the estimated total time the process will take, we divide the seconds elapsed by the proportion of progress
					// For example:
					// 0.1 (10%) at 10 seconds == Total process will take 100 seconds
					// 0.5 (50%) at 20 seconds == Total process will take 40 seconds

					totalSeconds := float64(secondsElapsed)/currentProcessProgress

					estimatedSecondsRemaining := int64(totalSeconds) - secondsElapsed

					estimatedTimeRemainingTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(estimatedSecondsRemaining, false)
					if (err != nil) { return err }

					estimatedTimeRemainingBinding.Set("Estimated Time Remaining: " + estimatedTimeRemainingTranslated)

					time.Sleep(time.Second)
				}

				// This cannot be reached
			}

			err := updateTimeRemainingDisplayFunction()
			if (err != nil){

				trainModelIsStoppedBoolMutex.Lock()
				trainModelIsStoppedBool = true
				trainModelIsStoppedBoolMutex.Unlock()

				fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
				return
			}
		}

		go startUpdateTimeRemainingDisplayFunction()

		//Outputs:
		//	-bool: Process completed (true == was not stopped mid-way)
		//	-[]float32: Costs list (should decrease as training proceeds)
		//	-error
		trainModel := func()(bool, []float32, error){

			_, err := localFilesystem.CreateFolder("./TrainedModels")
			if (err != nil) { return false, nil, err }

			trainingSetFilepathsList, _, err := getTrainingAndTestingDataFilepathLists(attributeName)
			if (err != nil) { return false, nil, err }

			// Now we deterministically randomize the order of the trainingSetFilepathsList
			pseudorandomNumberGenerator := mathRand.New(mathRand.NewPCG(1, 2))

			pseudorandomNumberGenerator.Shuffle(len(trainingSetFilepathsList), func(i int, j int){
				trainingSetFilepathsList[i], trainingSetFilepathsList[j] = trainingSetFilepathsList[j], trainingSetFilepathsList[i]
			})

			// We create a new neural network object to train
			neuralNetworkObject, err := geneticPrediction.GetNewUntrainedNeuralNetworkObject(attributeName)
			if (err != nil) { return false, nil, err }

			// We get the quantity of training rounds to run
			// This is the number of rounds of training for the training data set
			// For example, if the quantity is 2, we train over the training set twice

			getTotalQuantityOfRoundsToRun := func()(int, error){

				switch attributeName{

					case "Eye Color":{
						return 1, nil
					}
					case "Height",
						"Obesity":{

						return 2, nil
					}
					case "Autism":{

						return 3, nil
					}
					case "Homosexualness":{
						return 3, nil
					}
					case "Lactose Tolerance":{
						return 10, nil
					}
				}

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

			totalQuantityOfRoundsToRun, err := getTotalQuantityOfRoundsToRun()
			if (err != nil){ return false, nil, err }

			quantityOfTrainingDatasInSet := len(trainingSetFilepathsList)

			quantityOfTrainingDatas := len(trainingSetFilepathsList) * totalQuantityOfRoundsToRun
			quantityOfTrainingDatasString := helpers.ConvertIntToString(quantityOfTrainingDatas)

			// This keeps track of how many training rounds we have completed
			// With each round, we shuffle the training data list and train the model again
			trainingRoundsCompleted := 0

			// This keeps track of how far along we are in training
			trainingDataIndex := 0

			// This keeps track of how many examples we have trained during all rounds
			quantityOfExamplesTrained := 0

			// Outputs:
			//	-bool: User stopped training
			//	-bool: Another training data exists
			//	-geneticPrediction.TrainingData
			//	-error
			getNextTrainingDataFunction := func()(bool, bool, geneticPrediction.TrainingData, error){

				trainModelIsStoppedBoolMutex.RLock()
				trainModelIsStopped := trainModelIsStoppedBool
				trainModelIsStoppedBoolMutex.RUnlock()

				if (trainModelIsStopped == true){
					// User exited the process
					return true, false, geneticPrediction.TrainingData{}, nil
				}

				if (trainingDataIndex == quantityOfTrainingDatasInSet){
					// We are done training this set

					trainingRoundsCompleted += 1

					if (trainingRoundsCompleted == totalQuantityOfRoundsToRun){
						// We are done training
						return false, false, geneticPrediction.TrainingData{}, nil
					}

					// We train another round
					trainingDataIndex = 0

					// We deterministically randomize the order of the training data for the next round

					pseudorandomNumberGenerator.Shuffle(len(trainingSetFilepathsList), func(i int, j int){
						trainingSetFilepathsList[i], trainingSetFilepathsList[j] = trainingSetFilepathsList[j], trainingSetFilepathsList[i]
					})
				}

				trainingDataFilepath := trainingSetFilepathsList[trainingDataIndex]

				fileExists, fileContents, err := localFilesystem.GetFileContents(trainingDataFilepath)
				if (err != nil) { return false, false, geneticPrediction.TrainingData{}, err }
				if (fileExists == false){
					return false, false, geneticPrediction.TrainingData{}, errors.New("TrainingData file not found: " + trainingDataFilepath)
				}

				trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents)
				if (err != nil) { return false, false, geneticPrediction.TrainingData{}, err }

				trainingDataIndex += 1
				quantityOfExamplesTrained += 1

				quantityOfExamplesTrainedString := helpers.ConvertIntToString(quantityOfExamplesTrained)
				numberOfExamplesProgress := "Trained " + quantityOfExamplesTrainedString + "/" + quantityOfTrainingDatasString + " Examples"

				progressDetailsBinding.Set(numberOfExamplesProgress)

				newProgressFloat64 := float64(quantityOfExamplesTrained)/float64(quantityOfTrainingDatas)

				err = progressPercentageBinding.Set(newProgressFloat64)
				if (err != nil) { return false, false, geneticPrediction.TrainingData{}, err }

				processProgressMutex.Lock()
				processProgress = newProgressFloat64
				processProgressMutex.Unlock()

				return false, true, trainingDataObject, nil
			}

			getAttributeIsNumericBool := func()(bool, error){

				switch attributeName{

					case "Height",
						"Autism",
						"Homosexualness",
						"Obesity":{
						return true, nil
					}
					case "Lactose Tolerance",
						"Eye Color":{
						return false, nil
					}
				}

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

			attributeIsNumeric, err := getAttributeIsNumericBool()
			if (err != nil) { return false, nil, err }

			processCompleted, costsList, err := geneticPrediction.TrainNeuralNetwork(attributeName, attributeIsNumeric, neuralNetworkObject, getNextTrainingDataFunction)
			if (err != nil) { return false, nil, err }
			if (processCompleted == false){
				return false, nil, nil
			}

			// Network training is complete.
			// We now save the neural network as a .gob file

			progressDetailsBinding.Set("Saving trained model to a file...")

			neuralNetworkBytes, err := geneticPredictionModels.EncodeNeuralNetworkObjectToBytes(*neuralNetworkObject)
			if (err != nil) { return false, nil, err }

			attributeNameWithoutWhitespaces := strings.ReplaceAll(attributeName, " ", "")

			neuralNetworkFilename := attributeNameWithoutWhitespaces + "Model.gob"

			err = localFilesystem.CreateOrOverwriteFile(neuralNetworkBytes, "./TrainedModels/", neuralNetworkFilename)
			if (err != nil) { return false, nil, err }

			progressPercentageBinding.Set(1)

			return true, costsList, nil
		}

		processIsComplete, costsList, err := trainModel()
		if (err != nil){

			trainModelIsStoppedBoolMutex.Lock()
			trainModelIsStoppedBool = true
			trainModelIsStoppedBoolMutex.Unlock()

			fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
			return
		}
		if (processIsComplete == false){
			// User exited the page
			return
		}

		fyne.Do(func(){setTrainModelIsCompletePage(window, costsList)})
	}

	go trainModelFunction()
}


func setTrainModelIsCompletePage(window fyne.Window, costsList []float32){

	title := getBoldLabelCentered("Training Model Is Complete")

	description1 := getLabelCentered("Model training is complete!")
	description2 := getLabelCentered("The model has been saved in the TrainedModels folder.")
	description3 := getLabelCentered("Below is a graph of the average cost during training.")

	exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), func(){
			setHomePage(window)
		}))

	header := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), exitButton, widget.NewSeparator())

	// We plot the average cost on a graph
	// We need to average everything to 100 points on a graph

	getCostsGraph := func()(image.Image, error){

		if (len(costsList) < 100){
			return nil, errors.New("CostsList has fewer than 100 entries.")
		}

		sizeOfEachGroup := len(costsList)/100

		averagedCostsList := make([]float64, 0)

		groupSummedCosts := float64(0)
		groupCostsCount := 0

		for _, costValue := range costsList{

			groupSummedCosts += float64(costValue)

			groupCostsCount += 1

			if (groupCostsCount >= sizeOfEachGroup){
				// Group has been created
				groupAverage := groupSummedCosts/float64(groupCostsCount)

				averagedCostsList = append(averagedCostsList, groupAverage)

				// We reset these variables
				groupSummedCosts = 0
				groupCostsCount = 0
			}
		}

		// Now we construct the chart line graph item

		newLineDatumsList := make([]statisticsDatum.NumericStatisticsDatum, 0, len(averagedCostsList))

		for index, averageCost := range averagedCostsList{

			xAxisValue := float64((index+1))

			newNumericStatisticsDatum := statisticsDatum.NumericStatisticsDatum{
				XAxisValue: xAxisValue,
				YAxisValue: averageCost,
			}

			newLineDatumsList = append(newLineDatumsList, newNumericStatisticsDatum)
		}

		newChartLineData := createCharts.ChartLineData{
			Name: "",
			LineDatumsList: newLineDatumsList,
		}

		newChartLineDatasList := []createCharts.ChartLineData{newChartLineData}

		formatXAxisValuesFunction := func(input float64)(string, error){

			result := helpers.ConvertFloat64ToStringRounded(input, 0)

			return result, nil
		}

		formatYAxisValuesFunction := func(input float64)(string, error){

			result := helpers.ConvertFloat64ToStringRounded(input, 3)

			return result, nil
		}

		newChartImage, err := createCharts.CreateLineChart("Average Cost During Training", newChartLineDatasList, "Time", formatXAxisValuesFunction, "Average Cost", formatYAxisValuesFunction, false, "")
		if (err != nil) { return nil, err }

		return newChartImage, nil
	}

	costsGraphImage, err := getCostsGraph()
	if (err != nil){
		setErrorEncounteredPage(window, err, func(){setHomePage(window)})
		return
	}

	chartFyneImage := canvas.NewImageFromImage(costsGraphImage)
	chartFyneImage.FillMode = canvas.ImageFillContain

	page := container.NewBorder(header, nil, nil, nil, chartFyneImage)

	window.SetContent(page)
}


func setTestModelsPage(window fyne.Window, previousPage func()){

	currentPage := func(){setTestModelsPage(window, previousPage)}

	title := getBoldLabelCentered("Test Models")

	backButton := getBackButtonCentered(previousPage)

	description1 := getLabelCentered("Press the button below to begin testing a genetic model.")
	description2 := getLabelCentered("This will test each neural network using user training data examples.")
	description3 := getLabelCentered("The testing data is not used to train the models.")
	description4 := getLabelCentered("The results of the testing will be displayed at the end.")
	description5 := getLabelCentered("The results will also be saved in the ModelAccuracies folder.")
	description6 := getLabelCentered("You must select a model to test.")

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

	attributeNameSelector := widget.NewSelect(attributeNamesList, nil)

	beginTestingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Testing Model", theme.MediaPlayIcon(), func(){

			selectedAttributeIndex := attributeNameSelector.SelectedIndex()
			if (selectedAttributeIndex < 0){
				title := "No Attribute Selected"
				dialogMessage1 := getLabelCentered("You must select a model to test.")
				dialogContent := container.NewVBox(dialogMessage1)
				dialog.ShowCustom(title, "Close", dialogContent, window)
				return
			}

			attributeName := attributeNameSelector.Selected

			setStartAndMonitorTestModelPage(window, attributeName, currentPage)
		}))

	attributeNameSelectorCentered := getWidgetCentered(attributeNameSelector)

	page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, widget.NewSeparator(), attributeNameSelectorCentered, widget.NewSeparator(), beginTestingButton)

	window.SetContent(page)
}

func setStartAndMonitorTestModelPage(window fyne.Window, attributeName string, previousPage func()){

	title := getBoldLabelCentered("Testing Model")

	modelNameTitle := getLabelCentered("Model Name:")

	modelNameLabel := getBoldLabelCentered(attributeName)

	progressDetailsTitle := getBoldLabelCentered("Progress Details:")

	progressDetailsBinding := binding.NewString()
	progressPercentageBinding := binding.NewFloat()

	loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))

	progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding)
	progressDetailsLabel.TextStyle = fyne.TextStyle{
		Bold: false,
		Italic: true,
		Monospace: false,
	}

	progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel)

	// We set this bool to true to stop the testModel process
	var testModelIsStoppedBoolMutex sync.RWMutex
	testModelIsStoppedBool := false

	cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){

			testModelIsStoppedBoolMutex.Lock()
			testModelIsStoppedBool = true
			testModelIsStoppedBoolMutex.Unlock()

			previousPage()
		}))

	page := container.NewVBox(title, widget.NewSeparator(), modelNameTitle, modelNameLabel, widget.NewSeparator(), progressDetailsTitle, loadingBar, progressDetailsLabelCentered, widget.NewSeparator(), cancelButton)

	window.SetContent(page)

	getAttributeIsNumericBool := func()(bool, error){

		switch attributeName{

			case "Height",
				"Autism",
				"Homosexualness",
				"Obesity":{
				return true, nil
			}
			case "Lactose Tolerance",
				"Eye Color":{
				return false, nil
			}
		}

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

	attributeIsNumeric, err := getAttributeIsNumericBool()
	if (err != nil) {
		setErrorEncounteredPage(window, errors.New("setStartAndMonitorTestModelPage called with unknown attributeName: " + attributeName), previousPage)
		return
	}

	if (attributeIsNumeric == false){

		// attribute is a Discrete trait

		testModelFunction := func(){

			//Outputs:
			//	-bool: Process completed (true == was not stopped mid-way)
			//	-geneticPrediction.DiscreteTraitPredictionAccuracyInfoMap
			//	-error
			testModel := func()(bool, trainedPredictionModels.DiscreteTraitPredictionAccuracyInfoMap, error){

				attributeRSIDsList, err := geneticPrediction.GetAttributeLociList(attributeName)
				if (err != nil) { return false, trainedPredictionModels.DiscreteTraitPredictionAccuracyInfoMap{}, err }

				totalQuantityOfLoci := len(attributeRSIDsList)

				type TraitAccuracyStatisticsValue struct{

					// This stores the quantity of examples of this outcome
					// In other words: The quantity of examples for people in the training data who had this outcome
					QuantityOfPredictions_Input int

					// This stores the quantity of predictions that were made for this outcome
					// In other words: The quantity of instances where our model predicted this outcome
					QuantityOfPredictions_Output int

					// This stores the quantity of predictions that were correct when the person whose genome was sequenced had this outcome
					QuantityOfCorrectPredictions_Input int

					// This stores the quantity of predictions that were correct when the model predicted this outcome
					QuantityOfCorrectPredictions_Output int
				}

				// We use this map to count up the information about predictions
				// We use information from this map to construct the final accuracy information map
				traitPredictionInfoMap := make(map[trainedPredictionModels.DiscreteTraitOutcomeInfo]TraitAccuracyStatisticsValue)


				_, testingSetFilepathsList, err := getTrainingAndTestingDataFilepathLists(attributeName)
				if (err != nil) { return false, nil, err }

				traitNameWithoutWhitespaces := strings.ReplaceAll(attributeName, " ", "")

				// We read the trained model for this trait
				modelFilename := traitNameWithoutWhitespaces + "Model.gob"

				trainedModelFilepath := goFilepath.Join("./TrainedModels/", modelFilename)

				fileExists, fileContents, err := localFilesystem.GetFileContents(trainedModelFilepath)
				if (err != nil) { return false, nil, err }
				if (fileExists == false){
					return false, nil, errors.New("TrainedModel not found: " + trainedModelFilepath)
				}

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

				numberOfTrainingDatas := len(testingSetFilepathsList)
				numberOfTrainingDatasString := helpers.ConvertIntToString(numberOfTrainingDatas)

				finalIndex := numberOfTrainingDatas - 1

				for index, filePath := range testingSetFilepathsList{

					testModelIsStoppedBoolMutex.RLock()
					testModelIsStopped := testModelIsStoppedBool
					testModelIsStoppedBoolMutex.RUnlock()

					if (testModelIsStopped == true){
						// User exited the process
						return false, nil, nil
					}

					fileExists, fileContents, err := localFilesystem.GetFileContents(filePath)
					if (err != nil) { return false, nil, err }
					if (fileExists == false){
						return false, nil, errors.New("TrainingData file not found: " + filePath)
					}

					trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents)
					if (err != nil) { return false, nil, err }

					trainingDataInputLayer := trainingDataObject.InputLayer
					trainingDataExpectedOutputLayer := trainingDataObject.OutputLayer
					trainingDataQuantityOfLociKnown := trainingDataObject.QuantityOfLociKnown
					trainingDataQuantityOfLociKnownAndPhased := trainingDataObject.QuantityOfLociKnownAndPhased

					predictionLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(&neuralNetworkObject, false, trainingDataInputLayer)
					if (err != nil) { return false, nil, err }

					numberOfPredictionNeurons := len(predictionLayer)

					if (len(trainingDataExpectedOutputLayer) != numberOfPredictionNeurons){
						return false, nil, errors.New("Neural network prediction output length does not match expected output length.")
					}

					correctOutcomeName, err := geneticPrediction.GetDiscreteOutcomeNameFromOutputLayer(attributeName, true, trainingDataExpectedOutputLayer)
					if (err != nil) { return false, nil, err }

					predictedOutcomeName, err := geneticPrediction.GetDiscreteOutcomeNameFromOutputLayer(attributeName, true, predictionLayer)
					if (err != nil) { return false, nil, err }

					getPredictionIsCorrectBool := func()bool{
						if (predictedOutcomeName == correctOutcomeName){
							return true
						}
						return false
					}

					predictionIsCorrect := getPredictionIsCorrectBool()

					proportionOfLociTested := float64(trainingDataQuantityOfLociKnown)/float64(totalQuantityOfLoci)
					percentageOfLociTested := int(100*proportionOfLociTested)

					proportionOfPhasedLoci := float64(trainingDataQuantityOfLociKnownAndPhased)/float64(trainingDataQuantityOfLociKnown)
					percentageOfPhasedLoci := int(100*proportionOfPhasedLoci)

					{
						// We first add the information to the map for when this outcome was the input of the model

						newTraitOutcomeInfo_Input := trainedPredictionModels.DiscreteTraitOutcomeInfo{

							OutcomeName: correctOutcomeName,
							PercentageOfLociTested: percentageOfLociTested,
							PercentageOfPhasedLoci: percentageOfPhasedLoci,
						}

						getTraitAccuracyStatisticsValue_Input := func()TraitAccuracyStatisticsValue{

							existingTraitAccuracyStatisticsValue, exists := traitPredictionInfoMap[newTraitOutcomeInfo_Input]
							if (exists == false){
								newTraitAccuracyStatisticsValue := TraitAccuracyStatisticsValue{}
								return newTraitAccuracyStatisticsValue
							}
							return existingTraitAccuracyStatisticsValue
						}

						traitAccuracyStatisticsValue := getTraitAccuracyStatisticsValue_Input()

						traitAccuracyStatisticsValue.QuantityOfPredictions_Input += 1

						if (predictionIsCorrect == true){
							traitAccuracyStatisticsValue.QuantityOfCorrectPredictions_Input += 1
						}

						traitPredictionInfoMap[newTraitOutcomeInfo_Input] = traitAccuracyStatisticsValue
					}

					{
						// We next add the information to the map for when this outcome was the output of the model

						newTraitOutcomeInfo_Output := trainedPredictionModels.DiscreteTraitOutcomeInfo{

							OutcomeName: predictedOutcomeName,
							PercentageOfLociTested: percentageOfLociTested,
							PercentageOfPhasedLoci: percentageOfPhasedLoci,
						}

						getTraitAccuracyStatisticsValue_Output := func()TraitAccuracyStatisticsValue{

							existingTraitAccuracyStatisticsValue, exists := traitPredictionInfoMap[newTraitOutcomeInfo_Output]
							if (exists == false){
								newTraitAccuracyStatisticsValue := TraitAccuracyStatisticsValue{}
								return newTraitAccuracyStatisticsValue
							}

							return existingTraitAccuracyStatisticsValue
						}

						traitAccuracyStatisticsValue := getTraitAccuracyStatisticsValue_Output()

						traitAccuracyStatisticsValue.QuantityOfPredictions_Output += 1

						if (predictionIsCorrect == true){
							traitAccuracyStatisticsValue.QuantityOfCorrectPredictions_Output += 1
						}

						traitPredictionInfoMap[newTraitOutcomeInfo_Output] = traitAccuracyStatisticsValue
					}

					exampleIndexString := helpers.ConvertIntToString(index+1)
					numberOfExamplesProgress := "Tested " + exampleIndexString + "/" + numberOfTrainingDatasString + " Examples"

					progressDetailsBinding.Set(numberOfExamplesProgress)

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

					progressPercentageBinding.Set(newProgressFloat64)
				}

				// Now we construct the TraitAccuracyInfoMap

				// This map stores the accuracy for each outcome
				traitPredictionAccuracyInfoMap := make(map[trainedPredictionModels.DiscreteTraitOutcomeInfo]trainedPredictionModels.DiscreteTraitPredictionAccuracyInfo)

				for traitPredictionInfo, value := range traitPredictionInfoMap{

					quantityOfPredictions_Input := value.QuantityOfPredictions_Input
					quantityOfPredictions_Output := value.QuantityOfPredictions_Output

					quantityOfCorrectPredictions_Input := value.QuantityOfCorrectPredictions_Input
					quantityOfCorrectPredictions_Output := value.QuantityOfCorrectPredictions_Output

					if (quantityOfCorrectPredictions_Input > quantityOfPredictions_Input){
						return false, nil, errors.New("traitPredictionInfoMap contains quantityOfCorrectPredictions_Input > quantityOfPredictions_Input")
					}
					if (quantityOfCorrectPredictions_Output > quantityOfPredictions_Output){
						return false, nil, errors.New("traitPredictionInfoMap contains quantityOfCorrectPredictions_Output > quantityOfPredictions_Output")
					}

					newTraitPredictionAccuracyInfo := trainedPredictionModels.DiscreteTraitPredictionAccuracyInfo{
						QuantityOfPredictions_Input: quantityOfPredictions_Input,
						QuantityOfPredictions_Output: quantityOfPredictions_Output,
					}

					if (quantityOfPredictions_Input > 0){

						proportionOfCorrectPredictions := float64(quantityOfCorrectPredictions_Input)/float64(quantityOfPredictions_Input)
						percentageOfCorrectPredictions := int(100*proportionOfCorrectPredictions)

						newTraitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Input = percentageOfCorrectPredictions
					}
					if (quantityOfPredictions_Output > 0){

						proportionOfCorrectPredictions := float64(quantityOfCorrectPredictions_Output)/float64(quantityOfPredictions_Output)
						percentageOfCorrectPredictions := int(100*proportionOfCorrectPredictions)

						newTraitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Output = percentageOfCorrectPredictions
					}

					traitPredictionAccuracyInfoMap[traitPredictionInfo] = newTraitPredictionAccuracyInfo
				}

				// Testing is complete.

				// We save the info map as a file in the ModelAccuracies folder

				fileBytes, err := trainedPredictionModels.EncodeDiscreteTraitPredictionAccuracyInfoMapToBytes(traitPredictionAccuracyInfoMap)
				if (err != nil) { return false, nil, err }

				_, err = localFilesystem.CreateFolder("./ModelAccuracies")
				if (err != nil) { return false, nil, err }

				modelAccuracyFilename := traitNameWithoutWhitespaces + "ModelAccuracy.gob"

				err = localFilesystem.CreateOrOverwriteFile(fileBytes, "./ModelAccuracies/", modelAccuracyFilename)
				if (err != nil) { return false, nil, err }

				progressPercentageBinding.Set(1)

				return true, traitPredictionAccuracyInfoMap, nil
			}

			processIsComplete, traitPredictionAccuracyInfoMap, err := testModel()
			if (err != nil){
				fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
				return
			}
			if (processIsComplete == false){
				// User exited the page
				return
			}

			fyne.Do(func(){setViewModelTestingDiscreteTraitResultsPage(window, attributeName, traitPredictionAccuracyInfoMap, previousPage)})
		}

		go testModelFunction()
	
		return

	} else {

		// attribute is Numeric

		testModelFunction := func(){

			//Outputs:
			//	-bool: Process completed (true == was not stopped mid-way)
			//	-geneticPrediction.NumericAttributePredictionAccuracyInfo
			//	-error
			testModel := func()(bool, trainedPredictionModels.NumericAttributePredictionAccuracyInfo, error){

				attributeRSIDsList, err := geneticPrediction.GetAttributeLociList(attributeName)
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

				totalQuantityOfLoci := len(attributeRSIDsList)

				// Map Structure: NumericAttributePredictionInfo_Input -> []float64 (List of distances for each prediction)
				attributePredictionAccuracyInfoMap_Input := make(map[trainedPredictionModels.NumericAttributePredictionInfo_Input][]float64)

				// Map Structure: NumericAttributePredictionInfo_Output -> []float64 (List of distances for each prediction)
				attributePredictionAccuracyInfoMap_Output := make(map[trainedPredictionModels.NumericAttributePredictionInfo_Output][]float64)

				_, testingSetFilepathsList, err := getTrainingAndTestingDataFilepathLists(attributeName)
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

				attributeNameWithoutWhitespaces := strings.ReplaceAll(attributeName, " ", "")

				// We read the trained model for this attribute
				modelFilename := attributeNameWithoutWhitespaces + "Model.gob"

				trainedModelFilepath := goFilepath.Join("./TrainedModels/", modelFilename)

				fileExists, fileContents, err := localFilesystem.GetFileContents(trainedModelFilepath)
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }
				if (fileExists == false){
					return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, errors.New("TrainedModel not found: " + trainedModelFilepath)
				}

				neuralNetworkObject, err := geneticPredictionModels.DecodeBytesToNeuralNetworkObject(fileContents)
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

				numberOfTrainingDatas := len(testingSetFilepathsList)
				numberOfTrainingDatasString := helpers.ConvertIntToString(numberOfTrainingDatas)

				finalIndex := numberOfTrainingDatas - 1

				for index, filePath := range testingSetFilepathsList{

					testModelIsStoppedBoolMutex.RLock()
					testModelIsStopped := testModelIsStoppedBool
					testModelIsStoppedBoolMutex.RUnlock()

					if (testModelIsStopped == true){
						// User exited the process
						return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, nil
					}

					fileExists, fileContents, err := localFilesystem.GetFileContents(filePath)
					if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }
					if (fileExists == false){
						return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, errors.New("TrainingData file not found: " + filePath)
					}

					trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents)
					if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

					trainingDataInputLayer := trainingDataObject.InputLayer
					trainingDataExpectedOutputLayer := trainingDataObject.OutputLayer
					trainingDataQuantityOfKnownLoci := trainingDataObject.QuantityOfLociKnown
					trainingDataQuantityOfKnownLociKnownAndPhased := trainingDataObject.QuantityOfLociKnownAndPhased

					if (len(trainingDataExpectedOutputLayer) != 1){
						return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, errors.New("Neural network training data prediction output layer length is not 1.")
					}

					predictionLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(&neuralNetworkObject, true, trainingDataInputLayer)
					if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

					if (len(predictionLayer) != 1){
						return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, errors.New("Neural network numeric prediction output layer length is not 1.")
					}

					correctOutcomeValue, err := geneticPrediction.GetNumericOutcomeValueFromOutputLayer(attributeName, trainingDataExpectedOutputLayer)
					if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

					predictedOutcomeValue, err := geneticPrediction.GetNumericOutcomeValueFromOutputLayer(attributeName, predictionLayer)
					if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

					proportionOfLociTested := float64(trainingDataQuantityOfKnownLoci)/float64(totalQuantityOfLoci)
					percentageOfLociTested := int(100*proportionOfLociTested)

					proportionOfPhasedLoci := float64(trainingDataQuantityOfKnownLociKnownAndPhased)/float64(trainingDataQuantityOfKnownLoci)
					percentageOfPhasedLoci := int(100*proportionOfPhasedLoci)

					distanceFromCorrectValue := math.Abs(predictedOutcomeValue - correctOutcomeValue)

					{
						newNumericAttributePredictionInfo_Input := trainedPredictionModels.NumericAttributePredictionInfo_Input{
							PercentageOfLociTested: percentageOfLociTested,
							PercentageOfPhasedLoci: percentageOfPhasedLoci,
							ExpectedValue: correctOutcomeValue,
						}

						existingList, exists := attributePredictionAccuracyInfoMap_Input[newNumericAttributePredictionInfo_Input]
						if (exists == false){
							attributePredictionAccuracyInfoMap_Input[newNumericAttributePredictionInfo_Input] = []float64{distanceFromCorrectValue}
						} else {
							existingList = append(existingList, distanceFromCorrectValue)
							attributePredictionAccuracyInfoMap_Input[newNumericAttributePredictionInfo_Input] = existingList
						}
					}
					{
						newNumericAttributePredictionInfo_Output := trainedPredictionModels.NumericAttributePredictionInfo_Output{
							PercentageOfLociTested: percentageOfLociTested,
							PercentageOfPhasedLoci: percentageOfPhasedLoci,
							PredictedValue: predictedOutcomeValue,
						}

						existingList, exists := attributePredictionAccuracyInfoMap_Output[newNumericAttributePredictionInfo_Output]
						if (exists == false){
							attributePredictionAccuracyInfoMap_Output[newNumericAttributePredictionInfo_Output] = []float64{distanceFromCorrectValue}
						} else {
							existingList = append(existingList, distanceFromCorrectValue)
							attributePredictionAccuracyInfoMap_Output[newNumericAttributePredictionInfo_Output] = existingList
						}
					}

					exampleIndexString := helpers.ConvertIntToString(index+1)
					numberOfExamplesProgress := "Tested " + exampleIndexString + "/" + numberOfTrainingDatasString + " Examples"

					progressDetailsBinding.Set(numberOfExamplesProgress)

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

					progressPercentageBinding.Set(newProgressFloat64)
				}

				// Testing is complete.

				newNumericAttributePredictionAccuracyInfo := trainedPredictionModels.NumericAttributePredictionAccuracyInfo{
					PredictionAccuracyInfoMap_Input: attributePredictionAccuracyInfoMap_Input,
					PredictionAccuracyInfoMap_Output: attributePredictionAccuracyInfoMap_Output,
				}

				// We save the info object as a file in the ModelAccuracies folder

				fileBytes, err := trainedPredictionModels.EncodeNumericAttributePredictionAccuracyInfoToBytes(newNumericAttributePredictionAccuracyInfo)
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

				_, err = localFilesystem.CreateFolder("./ModelAccuracies")
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

				modelAccuracyFilename := attributeNameWithoutWhitespaces + "ModelAccuracy.gob"

				err = localFilesystem.CreateOrOverwriteFile(fileBytes, "./ModelAccuracies/", modelAccuracyFilename)
				if (err != nil) { return false, trainedPredictionModels.NumericAttributePredictionAccuracyInfo{}, err }

				progressPercentageBinding.Set(1)

				return true, newNumericAttributePredictionAccuracyInfo, nil
			}

			processIsComplete, attributePredictionAccuracyInfo, err := testModel()
			if (err != nil){
				fyne.Do(func(){setErrorEncounteredPage(window, err, previousPage)})
				return
			}
			if (processIsComplete == false){
				// User exited the page
				return
			}

			fyne.Do(func(){setViewModelTestingNumericAttributeResultsPage(window, attributeName, attributePredictionAccuracyInfo, 80, previousPage)})
		}

		go testModelFunction()
	}
}

// This is a page to view the details of testing for a specific trait's model
func setViewModelTestingDiscreteTraitResultsPage(window fyne.Window, traitName string, traitAccuracyInfoMap trainedPredictionModels.DiscreteTraitPredictionAccuracyInfoMap, exitPage func()){

	currentPage := func(){setViewModelTestingDiscreteTraitResultsPage(window, traitName, traitAccuracyInfoMap, exitPage)}

	title := getBoldLabelCentered("Discrete Trait Prediction Accuracy Details")

	exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage))

	description1 := getLabelCentered("The results of the prediction accuracy for this trait are below.")

	traitNameTitle := widget.NewLabel("Trait Name:")
	traitNameLabel := getBoldLabel(traitName)
	traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameTitle, traitNameLabel, layout.NewSpacer())

	description2 := getLabelCentered("Prediction accuracy values are a pair of Input Accuracy/Output Accuracy.")
	description3 := getLabelCentered("Input Accuracy is the probability that a trait prediction that the model makes is correct for someone with that outcome.")
	description4 := getLabelCentered("Output Accuracy is the probability that, if the model predicts that outcome for any given genome, that prediction is accurate.")

	traitObject, err := traits.GetTraitObject(traitName)
	if (err != nil) {
		setErrorEncounteredPage(window, err, exitPage)
		return
	}

	traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric
	if (traitIsDiscreteOrNumeric != "Discrete"){
		setErrorEncounteredPage(window, errors.New("setViewModelTestingDiscreteTraitResultsPage called with non-discrete trait: " + traitName), exitPage)
		return
	}

	outcomeNamesList := traitObject.OutcomesList

	viewChartButton := widget.NewButtonWithIcon("View Chart", theme.InfoIcon(), func(){

			getChartImage := func()(image.Image, error){

				// We create a chart to display the prediction model accuracy
				// The chart is a line graph
				// The X-axis represents the percentage of known loci
				// The Y-axis represents the probability of a correct prediction
				// Each outcome has 2 lines: The input and output prediction accuracies

				newChartLineDatasList := make([]createCharts.ChartLineData, 0)

				for _, outcomeName := range outcomeNamesList{

					// We use this struct to count up the quantity of correct predictions and total predictions for each percentageOfKnownLoci
					type PredictionAccuracyInfo struct{

						QuantityOfCorrectPredictions int

						QuantityOfPredictions int
					}

					// Map Structure:
					//	Percentage of known loci (0-100) -> PredictionAccuracyInfo
					outcomePredictionsInfoMap_Input := make(map[int]PredictionAccuracyInfo)
					outcomePredictionsInfoMap_Output := make(map[int]PredictionAccuracyInfo)

					for traitOutcomeInfo, traitPredictionAccuracyInfo := range traitAccuracyInfoMap{

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

						percentageOfLociTested := traitOutcomeInfo.PercentageOfLociTested

						quantityOfPredictions_Input := traitPredictionAccuracyInfo.QuantityOfPredictions_Input
						quantityOfPredictions_Output := traitPredictionAccuracyInfo.QuantityOfPredictions_Output

						predictionAccuracyPercentage_Input := traitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Input
						predictionAccuracyPercentage_Output := traitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Output

						predictionAccuracyProportion_Input := float64(predictionAccuracyPercentage_Input)/100
						predictionAccuracyProportion_Output := float64(predictionAccuracyPercentage_Output)/100

						quantityOfCorrectPredictions_Input := float64(quantityOfPredictions_Input) * predictionAccuracyProportion_Input
						quantityOfCorrectPredictions_Output := float64(quantityOfPredictions_Output) * predictionAccuracyProportion_Output

						{
							existingPredictionAccuracyInfo, exists := outcomePredictionsInfoMap_Input[percentageOfLociTested]
							if (exists == false){

								newPredictionAccuracyInfo := PredictionAccuracyInfo{
									QuantityOfCorrectPredictions: int(quantityOfCorrectPredictions_Input),
									QuantityOfPredictions: quantityOfPredictions_Input,
								}
								outcomePredictionsInfoMap_Input[percentageOfLociTested] = newPredictionAccuracyInfo

							} else {

								existingPredictionAccuracyInfo.QuantityOfCorrectPredictions += int(quantityOfCorrectPredictions_Input)
								existingPredictionAccuracyInfo.QuantityOfPredictions += quantityOfPredictions_Input

								outcomePredictionsInfoMap_Input[percentageOfLociTested] = existingPredictionAccuracyInfo
							}
						}
						{
							existingPredictionAccuracyInfo, exists := outcomePredictionsInfoMap_Output[percentageOfLociTested]
							if (exists == false){

								newPredictionAccuracyInfo := PredictionAccuracyInfo{
									QuantityOfCorrectPredictions: int(quantityOfCorrectPredictions_Output),
									QuantityOfPredictions: quantityOfPredictions_Output,
								}
								outcomePredictionsInfoMap_Output[percentageOfLociTested] = newPredictionAccuracyInfo

							} else {

								existingPredictionAccuracyInfo.QuantityOfCorrectPredictions += int(quantityOfCorrectPredictions_Output)
								existingPredictionAccuracyInfo.QuantityOfPredictions += quantityOfPredictions_Output

								outcomePredictionsInfoMap_Output[percentageOfLociTested] = existingPredictionAccuracyInfo
							}
						}
					}

					// Now we contruct the line chart data objects

					//Outputs:
					//	-bool: Enough data exists to create a line chart object
					//	-createCharts.ChartLineData
					getChartLineObjectFromPredictionsInfoMap := func(lineName string, inputMap map[int]PredictionAccuracyInfo)(bool, createCharts.ChartLineData){

						lineDatumsList := make([]statisticsDatum.NumericStatisticsDatum, 0)

						for percentageOfLociTested, predictionAccuracyInfo := range inputMap{

							quantityOfCorrectPredictions := predictionAccuracyInfo.QuantityOfCorrectPredictions
							quantityOfPredictions := predictionAccuracyInfo.QuantityOfPredictions

							if (quantityOfPredictions < 5){
								// Not enough data to show a meaningful datapoint
								continue
							}

							probabilityOfCorrectPrediction := 100 * float64(quantityOfCorrectPredictions)/float64(quantityOfPredictions)

							lineDatumObject := statisticsDatum.NumericStatisticsDatum{
								XAxisValue: float64(percentageOfLociTested),
								YAxisValue: probabilityOfCorrectPrediction,
							}

							lineDatumsList = append(lineDatumsList, lineDatumObject)
						}

						if (len(lineDatumsList) < 2){
							return false, createCharts.ChartLineData{}
						}

						newChartLineData := createCharts.ChartLineData{
							Name: lineName,
							LineDatumsList: lineDatumsList,
						}

						return true, newChartLineData
					}

					newChartLineDataTitle_Input := outcomeName + " (Input)"
					enoughDataExists, newChartLineData_Input := getChartLineObjectFromPredictionsInfoMap(newChartLineDataTitle_Input, outcomePredictionsInfoMap_Input)
					if (enoughDataExists == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData_Input)
					}

					newChartLineDataTitle_Output := outcomeName + " (Output)"
					enoughDataExists, newChartLineData_Output := getChartLineObjectFromPredictionsInfoMap(newChartLineDataTitle_Output, outcomePredictionsInfoMap_Output)
					if (enoughDataExists == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData_Output)
					}
				}

				chartTitle := "Genetic Prediction Model Accuracy - " + traitName

				xAxisName := "Percentage Of Known Loci"

				yAxisName := "Average Probability Of Accurate Prediction"

				xAxisValueFormatter := func(inputValue float64)(string, error){

					result := helpers.ConvertFloat64ToStringRounded(inputValue, 0)

					resultFormatted := result + "%"

					return resultFormatted, nil
				}

				yAxisValueFormatter := func(inputValue float64)(string, error){

					result := helpers.ConvertFloat64ToStringRounded(inputValue, 0)

					return result, nil
				}

				yAxisUnitsProvided := true

				yAxisUnits := "%"

				newChartImage, err := createCharts.CreateLineChart(chartTitle, newChartLineDatasList, xAxisName, xAxisValueFormatter, yAxisName, yAxisValueFormatter, yAxisUnitsProvided, yAxisUnits)
				if (err != nil) { return nil, err }

				return newChartImage, nil
			}

			newChartImage, err := getChartImage()
			if (err != nil){
				setErrorEncounteredPage(window, err, currentPage)
				return
			}

			setViewChartImagePage(window, newChartImage, currentPage)
		})

	viewChartButtonCentered := getWidgetCentered(viewChartButton)

	getResultsGrid := func()(*fyne.Container, error){

		emptyLabel1 := widget.NewLabel("")
		outcomeNameTitle := getItalicLabelCentered("Outcome Name")

		predictionAccuracyTitle1 := getItalicLabelCentered("Prediction Accuracy")
		knownLociLabel_0to33 := getItalicLabelCentered("0-33% Known Loci")

		predictionAccuracyTitle2 := getItalicLabelCentered("Prediction Accuracy")
		knownLociLabel_34to66 := getItalicLabelCentered("34-66% Known Loci")

		predictionAccuracyTitle3 := getItalicLabelCentered("Prediction Accuracy")
		knownLociLabel_67to100 := getItalicLabelCentered("67-100% Known Loci")

		outcomeNameColumn := container.NewVBox(emptyLabel1, outcomeNameTitle, widget.NewSeparator())
		predictionAccuracyColumn_0to33 := container.NewVBox(predictionAccuracyTitle1, knownLociLabel_0to33, widget.NewSeparator())
		predictionAccuracyColumn_34to66 := container.NewVBox(predictionAccuracyTitle2, knownLociLabel_34to66, widget.NewSeparator())
		predictionAccuracyColumn_67to100 := container.NewVBox(predictionAccuracyTitle3, knownLociLabel_67to100, widget.NewSeparator())

		for _, outcomeName := range outcomeNamesList{

			outcomeNameLabel := getBoldLabelCentered(outcomeName)

			// We use the below variables to sum up the accuracy information to find the average probability of a correct prediction

			quantityOfCorrectPredictions_Input_0to33 := 0
			quantityOfPredictions_Input_0to33 := 0

			quantityOfCorrectPredictions_Output_0to33 := 0
			quantityOfPredictions_Output_0to33 := 0

			quantityOfCorrectPredictions_Input_34to66 := 0
			quantityOfPredictions_Input_34to66 := 0

			quantityOfCorrectPredictions_Output_34to66 := 0
			quantityOfPredictions_Output_34to66 := 0

			quantityOfCorrectPredictions_Input_67to100 := 0
			quantityOfPredictions_Input_67to100 := 0

			quantityOfCorrectPredictions_Output_67to100 := 0
			quantityOfPredictions_Output_67to100 := 0

			for traitOutcomeInfo, traitPredictionAccuracyInfo := range traitAccuracyInfoMap{

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

				percentageOfLociTested := traitOutcomeInfo.PercentageOfLociTested

				quantityOfPredictions_Input := traitPredictionAccuracyInfo.QuantityOfPredictions_Input
				quantityOfPredictions_Output := traitPredictionAccuracyInfo.QuantityOfPredictions_Output

				predictionAccuracyPercentage_Input := traitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Input
				predictionAccuracyPercentage_Output := traitPredictionAccuracyInfo.ProbabilityOfCorrectPrediction_Output

				predictionAccuracyProportion_Input := float64(predictionAccuracyPercentage_Input)/100
				predictionAccuracyProportion_Output := float64(predictionAccuracyPercentage_Output)/100

				quantityOfCorrectPredictions_Input := int(float64(quantityOfPredictions_Input) * predictionAccuracyProportion_Input)
				quantityOfCorrectPredictions_Output := int(float64(quantityOfPredictions_Output) * predictionAccuracyProportion_Output)

				if (percentageOfLociTested <= 33){

					quantityOfCorrectPredictions_Input_0to33 += quantityOfCorrectPredictions_Input
					quantityOfPredictions_Input_0to33 += quantityOfPredictions_Input

					quantityOfCorrectPredictions_Output_0to33 += quantityOfCorrectPredictions_Output
					quantityOfPredictions_Output_0to33 += quantityOfPredictions_Output

				} else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){

					quantityOfCorrectPredictions_Input_34to66 += quantityOfCorrectPredictions_Input
					quantityOfPredictions_Input_34to66 += quantityOfPredictions_Input

					quantityOfCorrectPredictions_Output_34to66 += quantityOfCorrectPredictions_Output
					quantityOfPredictions_Output_34to66 += quantityOfPredictions_Output
				} else {

					quantityOfCorrectPredictions_Input_67to100 += quantityOfCorrectPredictions_Input
					quantityOfPredictions_Input_67to100 += quantityOfPredictions_Input

					quantityOfCorrectPredictions_Output_67to100 += quantityOfCorrectPredictions_Output
					quantityOfPredictions_Output_67to100 += quantityOfPredictions_Output
				}
			}

			getAverageAccuracyLabel := func(quantityOfCorrectPredictions int, quantityOfPredictions int)fyne.Widget{
				if (quantityOfPredictions == 0){

					label := getItalicLabel("Unknown")

					return label
				}

				averageAccuracyPercentage := (float64(quantityOfCorrectPredictions)/float64(quantityOfPredictions))*100

				averageAccuracyString := helpers.ConvertFloat64ToStringRounded(averageAccuracyPercentage, 0)

				averageAccuracyFormatted := averageAccuracyString + "%"

				averageAccuracyLabel := getBoldLabel(averageAccuracyFormatted)

				return averageAccuracyLabel
			}

			averageAccuracyLabel_Input_0to33 := getAverageAccuracyLabel(quantityOfCorrectPredictions_Input_0to33, quantityOfPredictions_Input_0to33)
			averageAccuracyLabel_Input_34to66 := getAverageAccuracyLabel(quantityOfCorrectPredictions_Input_34to66, quantityOfPredictions_Input_34to66)
			averageAccuracyLabel_Input_67to100 := getAverageAccuracyLabel(quantityOfCorrectPredictions_Input_67to100, quantityOfPredictions_Input_67to100)

			averageAccuracyLabel_Output_0to33 := getAverageAccuracyLabel(quantityOfCorrectPredictions_Output_0to33, quantityOfPredictions_Output_0to33)
			averageAccuracyLabel_Output_34to66 := getAverageAccuracyLabel(quantityOfCorrectPredictions_Output_34to66, quantityOfPredictions_Output_34to66)
			averageAccuracyLabel_Output_67to100 := getAverageAccuracyLabel(quantityOfCorrectPredictions_Output_67to100, quantityOfPredictions_Output_67to100)

			averageAccuracyLabel_0to33 := container.NewHBox(layout.NewSpacer(), averageAccuracyLabel_Input_0to33, getBoldLabel("/"), averageAccuracyLabel_Output_0to33, layout.NewSpacer())
			averageAccuracyLabel_34to66 := container.NewHBox(layout.NewSpacer(), averageAccuracyLabel_Input_34to66, getBoldLabel("/"), averageAccuracyLabel_Output_34to66, layout.NewSpacer())
			averageAccuracyLabel_67to100 := container.NewHBox(layout.NewSpacer(), averageAccuracyLabel_Input_67to100, getBoldLabel("/"), averageAccuracyLabel_Output_67to100, layout.NewSpacer())

			outcomeNameColumn.Add(outcomeNameLabel)
			predictionAccuracyColumn_0to33.Add(averageAccuracyLabel_0to33)
			predictionAccuracyColumn_34to66.Add(averageAccuracyLabel_34to66)
			predictionAccuracyColumn_67to100.Add(averageAccuracyLabel_67to100)

			outcomeNameColumn.Add(widget.NewSeparator())
			predictionAccuracyColumn_0to33.Add(widget.NewSeparator())
			predictionAccuracyColumn_34to66.Add(widget.NewSeparator())
			predictionAccuracyColumn_67to100.Add(widget.NewSeparator())
		}

		resultsGrid := container.NewHBox(layout.NewSpacer(), outcomeNameColumn, predictionAccuracyColumn_0to33, predictionAccuracyColumn_34to66, predictionAccuracyColumn_67to100, layout.NewSpacer())

		return resultsGrid, nil
	}

	resultsGrid, err := getResultsGrid()
	if (err != nil){
		setErrorEncounteredPage(window, err, func(){setHomePage(window)})
		return
	}

	page := container.NewVBox(title, exitButton, widget.NewSeparator(), description1, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), description2, description3, description4, widget.NewSeparator(), viewChartButtonCentered, widget.NewSeparator(), resultsGrid)

	window.SetContent(page)
}


// This is a page to view the details of testing for a numeric attribute's model
func setViewModelTestingNumericAttributeResultsPage(window fyne.Window, attributeName string, attributeAccuracyInfo trainedPredictionModels.NumericAttributePredictionAccuracyInfo, accuracyPercentageToDisplay int, exitPage func()){

	if (accuracyPercentageToDisplay < 10 || accuracyPercentageToDisplay > 90){
		accuracyPercentageToDisplayString := helpers.ConvertIntToString(accuracyPercentageToDisplay)
		setErrorEncounteredPage(window, errors.New("setViewModelTestingNumericAttributeResultsPage called with invalid accuracyPercentageToDisplay: " + accuracyPercentageToDisplayString), func(){setHomePage(window)})
		return
	}

	currentPage := func(){setViewModelTestingNumericAttributeResultsPage(window, attributeName, attributeAccuracyInfo, accuracyPercentageToDisplay, exitPage)}

	title := getBoldLabelCentered("Numeric Attribute Prediction Accuracy Details")

	exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage))

	description1 := getLabelCentered("The results of the prediction accuracy for this attribute are below.")

	attributeNameTitle := widget.NewLabel("Attribute Name:")
	attributeNameLabel := getBoldLabel(attributeName)
	attributeNameRow := container.NewHBox(layout.NewSpacer(), attributeNameTitle, attributeNameLabel, layout.NewSpacer())

	description2 := getLabelCentered("Each cell contains a Input Accuracy Range/Output Accuracy Range")
	description3 := getLabelCentered("Input accuracy range is the range from the prediction you must travel for people who possess that value.")
	description4 := getLabelCentered("Output accuracy range is the range from the prediction you must travel for any random genome.")
	description5 := getLabelCentered("Each value is an average error range for each prediction to be accurate X% of the time.")
	description6 := getLabelCentered("For example, a height prediction of 6 feet is within +/-10 cm of the true value 90% of the time.")
	description7 := getLabelCentered("You can select which percentage of accuracy you want to view with the selector below.")

	accuracyRangeTitle := getBoldLabelCentered("Accuracy Range:")

	accuracySelector := widget.NewSelect([]string{"10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%"}, func(newSelection string){

			getNewSelectionInt := func()(int, error){

				selectionTrimmed := strings.TrimSuffix(newSelection, "%")

				selectionInt, err := helpers.ConvertStringToInt(selectionTrimmed)
				if (err != nil){
					return 0, errors.New("accuracySelector contains invalid selection: " + newSelection)
				}

				return selectionInt, nil
			}

			newSelectionInt, err := getNewSelectionInt()
			if (err != nil){
				setErrorEncounteredPage(window, err, func(){setHomePage(window)})
				return
			}

			if (newSelectionInt == accuracyPercentageToDisplay){
				return
			}

			setViewModelTestingNumericAttributeResultsPage(window, attributeName, attributeAccuracyInfo, newSelectionInt, exitPage)
		})

	accuracyPercentageToDisplayString := helpers.ConvertIntToString(accuracyPercentageToDisplay)
	accuracyPercentageToDisplayFormatted := accuracyPercentageToDisplayString + "%"

	accuracySelector.Selected = accuracyPercentageToDisplayFormatted

	accuracySelectorCentered := getWidgetCentered(accuracySelector)

	// We get the formatter for the distance values
	// This converts raw predictions to formatted values
	// Example: 100 -> "100 centimeters"

	getAttributeValueFormatter := func()(func(float64, bool)(string, error), error){

		// Currently Height is the only numeric attribute which needs to be formatted
		// The rest of the numeric attributes are all 0-10 values

		if (attributeName == "Height"){

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

			numericValueFormatter := traitObject.NumericValueFormatter

			return numericValueFormatter, nil
		}

		result := func(inputValue float64, showUnits bool)(string, error){

			// Input value is a value between 0 and 10

			inputValueFormatted := helpers.ConvertFloat64ToStringRounded(inputValue, 1)

			if (showUnits == false){
				return inputValueFormatted, nil
			}

			result := inputValueFormatted + "/10"

			return result, nil
		}

		return result, nil
	}

	attributeValueFormatter, err := getAttributeValueFormatter()
	if (err != nil){
		setErrorEncounteredPage(window, err, exitPage)
		return
	}

	// There are 2 charts by which to view the data:
	// 1: X-Axis == Attribute Value
	// 2: X-Axis == Percentage of Loci Known

	viewChart1Button := widget.NewButtonWithIcon("View Chart 1", theme.InfoIcon(), func(){

			getChartImage := func()(image.Image, error){

				predictionAccuracyInfoMap_Input := attributeAccuracyInfo.PredictionAccuracyInfoMap_Input
				predictionAccuracyInfoMap_Output := attributeAccuracyInfo.PredictionAccuracyInfoMap_Output

				// We use the below maps to store the prediction accuracy distances for each range of known loci for input/output values
				// We round every input/output value to the nearest integer: This groups many of the predictions and prevents the
				//		creation of too many datapoints, and also makes the chart less noisy.

				// This is a helper function to reduce duplicated code
				appendSliceValuesToDistancesMapEntry := func(inputMap map[int][]float64, predictionValue int, distancesToAppend []float64){

					existingList, exists := inputMap[predictionValue]
					if (exists == false){
						newSlice := slices.Clone(distancesToAppend)
						inputMap[predictionValue] = newSlice
					} else {
						existingList = append(existingList, distancesToAppend...)
						inputMap[predictionValue] = existingList
					}
				}

				// Map Structure: Prediction value (rounded) -> Slice of distances

				predictionAccuracyDistancesMap_Input_0to33 := make(map[int][]float64)
				predictionAccuracyDistancesMap_Input_34to66 := make(map[int][]float64)
				predictionAccuracyDistancesMap_Input_67to100 := make(map[int][]float64)

				predictionAccuracyDistancesMap_Output_0to33 := make(map[int][]float64)
				predictionAccuracyDistancesMap_Output_34to66 := make(map[int][]float64)
				predictionAccuracyDistancesMap_Output_67to100 := make(map[int][]float64)

				for attributePredictionInfo, attributePredictionAccuracyDistancesList := range predictionAccuracyInfoMap_Input{

					expectedValue := attributePredictionInfo.ExpectedValue
					percentageOfLociTested := attributePredictionInfo.PercentageOfLociTested

					expectedValueRounded := int(expectedValue)

					if (percentageOfLociTested <= 33){

						appendSliceValuesToDistancesMapEntry(predictionAccuracyDistancesMap_Input_0to33, expectedValueRounded, attributePredictionAccuracyDistancesList)

					} else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){

						appendSliceValuesToDistancesMapEntry(predictionAccuracyDistancesMap_Input_34to66, expectedValueRounded, attributePredictionAccuracyDistancesList)

					} else {

						appendSliceValuesToDistancesMapEntry(predictionAccuracyDistancesMap_Input_67to100, expectedValueRounded, attributePredictionAccuracyDistancesList)
					}
				}

				for attributePredictionInfo, attributePredictionAccuracyDistancesList := range predictionAccuracyInfoMap_Output{

					predictedValue := attributePredictionInfo.PredictedValue
					percentageOfLociTested := attributePredictionInfo.PercentageOfLociTested

					predictedValueRounded := int(predictedValue)

					if (percentageOfLociTested <= 33){

						appendSliceValuesToDistancesMapEntry(predictionAccuracyDistancesMap_Output_0to33, predictedValueRounded, attributePredictionAccuracyDistancesList)

					} else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){

						appendSliceValuesToDistancesMapEntry(predictionAccuracyDistancesMap_Output_34to66, predictedValueRounded, attributePredictionAccuracyDistancesList)

					} else {

						appendSliceValuesToDistancesMapEntry(predictionAccuracyDistancesMap_Output_67to100, predictedValueRounded, attributePredictionAccuracyDistancesList)
					}
				}

				//Outputs:
				//	-bool: Enough items exist (if false, no line will be created)
				//	-createCharts.ChartLineData: The data for the chart line
				//	-error
				getChartLineDataItemFromDistancesInfoMap := func(lineName string, inputDistancesInfoMap map[int][]float64)(bool, createCharts.ChartLineData, error){

					if (len(inputDistancesInfoMap) < 2){
						return false, createCharts.ChartLineData{}, nil
					}

					newLineDatumsList := make([]statisticsDatum.NumericStatisticsDatum, 0)

					for xAxisValue, distancesList := range inputDistancesInfoMap{

						averageDistance, err := helpers.GetAverageOfValuesInSliceOfFloat64s(distancesList)
						if (err != nil) { return false, createCharts.ChartLineData{}, err }

						newNumericStatisticsDatum := statisticsDatum.NumericStatisticsDatum{
							XAxisValue: float64(xAxisValue),
							YAxisValue: averageDistance,
						}

						newLineDatumsList = append(newLineDatumsList, newNumericStatisticsDatum)
					}

					newChartLineData := createCharts.ChartLineData{
						Name: lineName,
						LineDatumsList: newLineDatumsList,
					}

					return true, newChartLineData, nil
				}

				// We create at most 6 lines for our chart

				newChartLineDatasList := make([]createCharts.ChartLineData, 0)

				{
					lineCreated, newChartLineData, err := getChartLineDataItemFromDistancesInfoMap("0-33% Loci Known (Input)", predictionAccuracyDistancesMap_Input_0to33)
					if (err != nil){ return nil, err }
					if (lineCreated == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
					}
				}
				{
					lineCreated, newChartLineData, err := getChartLineDataItemFromDistancesInfoMap("34-66% Loci Known (Input)", predictionAccuracyDistancesMap_Input_34to66)
					if (err != nil){ return nil, err }
					if (lineCreated == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
					}
				}
				{
					lineCreated, newChartLineData, err := getChartLineDataItemFromDistancesInfoMap("67-100% Loci Known (Input)", predictionAccuracyDistancesMap_Input_67to100)
					if (err != nil){ return nil, err }
					if (lineCreated == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
					}
				}

				{
					lineCreated, newChartLineData, err := getChartLineDataItemFromDistancesInfoMap("0-33% Loci Known (Output)", predictionAccuracyDistancesMap_Output_0to33)
					if (err != nil){ return nil, err }
					if (lineCreated == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
					}
				}
				{
					lineCreated, newChartLineData, err := getChartLineDataItemFromDistancesInfoMap("34-66% Loci Known (Output)", predictionAccuracyDistancesMap_Output_34to66)
					if (err != nil){ return nil, err }
					if (lineCreated == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
					}
				}
				{
					lineCreated, newChartLineData, err := getChartLineDataItemFromDistancesInfoMap("67-100% Loci Known (Output)", predictionAccuracyDistancesMap_Output_67to100)
					if (err != nil){ return nil, err }
					if (lineCreated == true){
						newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
					}
				}

				chartTitle := "Genetic Prediction Model Accuracy - " + attributeName

				xAxisName := "Predicted/Expected " + attributeName

				yAxisName := "Average Prediction Distance/Error"

				xAxisValueFormatter := func(inputValue float64)(string, error){

					result, err := attributeValueFormatter(inputValue, false)
					if (err != nil) { return "", err }

					return result, nil
				}

				yAxisUnitsProvided := true

				yAxisUnits := ""

				yAxisValueFormatter := func(inputValue float64)(string, error){

					result, err := attributeValueFormatter(inputValue, false)
					if (err != nil) { return "", err }

					resultFormatted := "+/- " + result

					return resultFormatted, nil
				}

				newChartImage, err := createCharts.CreateLineChart(chartTitle, newChartLineDatasList, xAxisName, xAxisValueFormatter, yAxisName, yAxisValueFormatter, yAxisUnitsProvided, yAxisUnits)
				if (err != nil) { return nil, err }

				return newChartImage, nil
			}

			newChartImage, err := getChartImage()
			if (err != nil){
				setErrorEncounteredPage(window, err, currentPage)
				return
			}

			setViewChartImagePage(window, newChartImage, currentPage)
		})

		// For Chart 2, we use part of the process of creating the grid to retrieve the data, so we don't have duplicated code

		type Chart2LineInfo struct{
			// Name of the line
			// Example: 0-50 centimeters tall
			Name string

			// Map Structure: Percentage of loci tested -> Slice of prediction distances
			PredictionDistancesMap map[int][]float64
		}

		chart2LineInfosList := make([]Chart2LineInfo, 0)

		viewChart2Button := widget.NewButtonWithIcon("View Chart 2", theme.InfoIcon(), func(){

			//Outputs:
			//	-bool: Enough data exists to create chart
			//	-image.Image
			//	-error
			getChartImage := func()(bool, image.Image, error){

				newChartLineDatasList := make([]createCharts.ChartLineData, 0)

				for _, chartLineInfoObject := range chart2LineInfosList{

					chartLineName := chartLineInfoObject.Name
					predictionDistancesMap := chartLineInfoObject.PredictionDistancesMap

					if (len(predictionDistancesMap) < 2){
						// Not enough data exists for this item
						continue
					}

					newLineDatumsList := make([]statisticsDatum.NumericStatisticsDatum, 0)

					for xAxisValue, distancesList := range predictionDistancesMap{

						averageDistance, err := helpers.GetAverageOfValuesInSliceOfFloat64s(distancesList)
						if (err != nil) { return false, nil, err }

						newNumericStatisticsDatum := statisticsDatum.NumericStatisticsDatum{
							XAxisValue: float64(xAxisValue),
							YAxisValue: averageDistance,
						}

						newLineDatumsList = append(newLineDatumsList, newNumericStatisticsDatum)
					}

					newChartLineData := createCharts.ChartLineData{
						Name: chartLineName,
						LineDatumsList: newLineDatumsList,
					}

					newChartLineDatasList = append(newChartLineDatasList, newChartLineData)
				}

				if (len(newChartLineDatasList) == 0){
					return false, nil, nil
				}

				chartTitle := "Genetic Prediction Model Accuracy - " + attributeName

				xAxisName := "Percentage Of Loci Known"

				yAxisName := "Average Prediction Distance/Error"

				xAxisValueFormatter := func(inputValue float64)(string, error){

					inputValueString := helpers.ConvertFloat64ToStringRounded(inputValue, 0)

					inputValueFormatted := inputValueString + "%"

					return inputValueFormatted, nil
				}

				yAxisUnitsProvided := true

				yAxisUnits := ""

				yAxisValueFormatter := func(inputValue float64)(string, error){

					result, err := attributeValueFormatter(inputValue, false)
					if (err != nil) { return "", err }

					resultFormatted := "+/- " + result

					return resultFormatted, nil
				}

				newChartImage, err := createCharts.CreateLineChart(chartTitle, newChartLineDatasList, xAxisName, xAxisValueFormatter, yAxisName, yAxisValueFormatter, yAxisUnitsProvided, yAxisUnits)
				if (err != nil) { return false, nil, err }

				return true, newChartImage, nil
			}

			enoughDataExists, newChartImage, err := getChartImage()
			if (err != nil){
				setErrorEncounteredPage(window, err, currentPage)
				return
			}
			if (enoughDataExists == false){
				title := "Not Enough Data"
				dialogMessage1 := getLabelCentered("There is not enough data to create a chart for this model.")
				dialogContent := container.NewVBox(dialogMessage1)
				dialog.ShowCustom(title, "Close", dialogContent, window)
				return
			}

			setViewChartImagePage(window, newChartImage, currentPage)
		})

	viewChartButtonsGrid := container.NewGridWithColumns(2, viewChart1Button, viewChart2Button)

	viewChartButtonsGridCentered := getContainerCentered(viewChartButtonsGrid)

	getResultsGrid := func()(*fyne.Container, error){

		predictionAccuracyInfoMap_Input := attributeAccuracyInfo.PredictionAccuracyInfoMap_Input
		predictionAccuracyInfoMap_Output := attributeAccuracyInfo.PredictionAccuracyInfoMap_Output

		getMinimumAndMaximumAttributeValues := func()(float64, float64){

			if (attributeName != "Height"){
				// Currently, height is the only attribute which is not a 0-10 value
				return 0, 10
			}

			// First we store every single predicted and expected value in a slice

			allAttributeValues := make([]float64, 0)

			for numericAttributePredictionInfo_Input, _ := range predictionAccuracyInfoMap_Input{

				expectedValue := numericAttributePredictionInfo_Input.ExpectedValue

				allAttributeValues = append(allAttributeValues, expectedValue)
			}

			for numericAttributePredictionInfo_Output, _ := range predictionAccuracyInfoMap_Output{

				predictedValue := numericAttributePredictionInfo_Output.PredictedValue

				allAttributeValues = append(allAttributeValues, predictedValue)
			}

			// Now we retrieve the minimum and maximum values

			minimumPossibleValue := slices.Min(allAttributeValues)
			maximumPossibleValue := slices.Max(allAttributeValues)

			return minimumPossibleValue, maximumPossibleValue
		}

		minimumAttributeValue, maximumAttributeValue := getMinimumAndMaximumAttributeValues()

		if (minimumAttributeValue == maximumAttributeValue){
			return nil, errors.New("minimumAttributeValue == maximumAttributeValue when calculated from numeric trait prediction accuracy info map.")
		}

		// This function returns a slice of row values
		// Each value represents the start of a new collection of predicted/expected values
		getValueRowsSlice := func()([]float64, error){

			if (attributeName != "Height"){
				// Currently, height is the only attribute which is not a 0-10 value
				result := []float64{0, 2, 4, 6, 8}

				return result, nil
			}

			distanceBetweenValues := maximumAttributeValue - minimumAttributeValue

			sizeOfRowRange := distanceBetweenValues/float64(5)

			valueRowsSlice := []float64{minimumAttributeValue}

			currentValue := minimumAttributeValue

			rowsAdded := 1

			for {

				currentValue += sizeOfRowRange
				valueRowsSlice = append(valueRowsSlice, currentValue)

				rowsAdded += 1

				if (rowsAdded >= 5){

					if (currentValue >= maximumAttributeValue){
						return nil, errors.New("Rows loop logic fail encountered.")
					}

					break
				}
			}

			return valueRowsSlice, nil
		}

		valueRowsSlice, err := getValueRowsSlice()
		if (err != nil){ return nil, err }

		// We use this function to return a label which tells us the distance we must travel from a slice of
		//		distances to be accurate X% of the time
		getRequiredDistanceLabelFromDistancesSlice := func(distancesSlice []float64)(fyne.Widget, error){

			// We are trying to determine what distance we must travel to have our prediction be accurate X% of the time
			// We say that there must be at least 5 predictions for this value to be known
			// If there are fewer than 5 predictions, then we can't gauge the accuracy percentage for this range

			quantityOfPredictions := len(distancesSlice)

			if (quantityOfPredictions < 5){

				unknownLabel := getItalicLabel("Unknown")

				return unknownLabel, nil
			}

			// First we sort the slice in ascending order

			slices.Sort(distancesSlice)

			finalIndex := len(distancesSlice) - 1

			for index, distance := range distancesSlice{

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

				percentageOfPredictionsWithinDistance := int(100 * proportionOfPredictionsWithinDistance)

				if (percentageOfPredictionsWithinDistance >= accuracyPercentageToDisplay){

					// We have determined the distance required for this accuracy range

					distanceFormatted, err := attributeValueFormatter(distance, false)
					if (err != nil) { return nil, err }

					distanceWithPrefix := "+/- " + distanceFormatted

					distanceLabel := getBoldLabel(distanceWithPrefix)

					return distanceLabel, nil
				}
			}

			// This should never happen
			return nil, errors.New("Distance which meets proportion not found.")
		}

		// Now we create the grid header and columns.

		inputOrOutputTitle := getItalicLabelCentered("Input/Output")
		valueTitle := getItalicLabelCentered("Value")

		accuracyRangeTitle1 := getItalicLabelCentered("Accuracy Range")
		knownLociLabel_0to33 := getItalicLabelCentered("0-33% Known Loci")

		accuracyRangeTitle2 := getItalicLabelCentered("Accuracy Range")
		knownLociLabel_34to66 := getItalicLabelCentered("34-66% Known Loci")

		accuracyRangeTitle3 := getItalicLabelCentered("Accuracy Range")
		knownLociLabel_67to100 := getItalicLabelCentered("67-100% Known Loci")

		inputOrOutputValueColumn := container.NewVBox(inputOrOutputTitle, valueTitle, widget.NewSeparator())
		accuracyRangesColumn_0to33 := container.NewVBox(accuracyRangeTitle1, knownLociLabel_0to33, widget.NewSeparator())
		accuracyRangesColumn_34to66 := container.NewVBox(accuracyRangeTitle2, knownLociLabel_34to66, widget.NewSeparator())
		accuracyRangesColumn_67to100 := container.NewVBox(accuracyRangeTitle3, knownLociLabel_67to100, widget.NewSeparator())

		finalIndex := len(valueRowsSlice)-1

		for rowIndex, rowValueRangeStart := range valueRowsSlice{

			// This function returns a human-readable value to use for the end of the row range
			// Some formatted row examples: 10-19.99, 20-29.99
			getHumanReadableRowValueRangeEnd := func()float64{

				if (rowIndex == finalIndex){
					return maximumAttributeValue
				}

				nextRowRangeStartValue := valueRowsSlice[rowIndex+1]

				nextValueHumanReadable := nextRowRangeStartValue - 0.01

				return nextValueHumanReadable
			}

			humanReadableRowValueRangeEnd := getHumanReadableRowValueRangeEnd()

			rowValueRangeStartFormatted, err := attributeValueFormatter(rowValueRangeStart, true)
			if (err != nil) { return nil, err }

			rowValueRangeEndFormatted, err := attributeValueFormatter(humanReadableRowValueRangeEnd, true)
			if (err != nil) { return nil, err }

			inputOrOutputValueRangeFormatted := rowValueRangeStartFormatted + " - " + rowValueRangeEndFormatted

			inputOrOutputValueRangeLabel := getBoldLabelCentered(inputOrOutputValueRangeFormatted)


			// We use these to construct our graphical chart
			// Map Structure: Percentage of loci known -> Slice of prediction distances
			newChart2LineDistancesMap_Input := make(map[int][]float64)

			// We use these to construct our graphical chart
			// Map Structure: Percentage of loci known -> Slice of prediction distances
			newChart2LineDistancesMap_Output := make(map[int][]float64)

			// We use the below slices to collect all of the accuracy distances for each range of known loci for input/output values

			predictionAccuracyDistances_Input_0to33 := make([]float64, 0)
			predictionAccuracyDistances_Input_34to66 := make([]float64, 0)
			predictionAccuracyDistances_Input_67to100 := make([]float64, 0)

			predictionAccuracyDistances_Output_0to33 := make([]float64, 0)
			predictionAccuracyDistances_Output_34to66 := make([]float64, 0)
			predictionAccuracyDistances_Output_67to100 := make([]float64, 0)

			for attributePredictionInfo, attributePredictionAccuracyDistancesList := range predictionAccuracyInfoMap_Input{

				expectedValue := attributePredictionInfo.ExpectedValue

				if (expectedValue > maximumAttributeValue || expectedValue < minimumAttributeValue){
					return nil, errors.New("predictionAccuracyInfoMap_Input contains expected value out of range.")
				}

				if (expectedValue < rowValueRangeStart){
					continue
				}
				if (rowIndex != finalIndex){

					nextRowRangeStartValue := valueRowsSlice[rowIndex+1]

					if (expectedValue >= nextRowRangeStartValue){
						continue
					}
				}

				percentageOfLociTested := attributePredictionInfo.PercentageOfLociTested

				if (percentageOfLociTested <= 33){
					predictionAccuracyDistances_Input_0to33 = append(predictionAccuracyDistances_Input_0to33, attributePredictionAccuracyDistancesList...)

				} else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){

					predictionAccuracyDistances_Input_34to66 = append(predictionAccuracyDistances_Input_34to66, attributePredictionAccuracyDistancesList...)

				} else {

					predictionAccuracyDistances_Input_67to100 = append(predictionAccuracyDistances_Input_67to100, attributePredictionAccuracyDistancesList...)
				}

				existingList, exists := newChart2LineDistancesMap_Input[percentageOfLociTested]
				if (exists == false){
					newList := slices.Clone(attributePredictionAccuracyDistancesList)
					newChart2LineDistancesMap_Input[percentageOfLociTested] = newList
				} else {
					existingList = append(existingList, attributePredictionAccuracyDistancesList...)
					newChart2LineDistancesMap_Input[percentageOfLociTested] = existingList
				}
			}

			for attributePredictionInfo, attributePredictionAccuracyDistancesList := range predictionAccuracyInfoMap_Output{

				predictedValue := attributePredictionInfo.PredictedValue

				if (predictedValue > maximumAttributeValue || predictedValue < minimumAttributeValue){
					return nil, errors.New("predictionAccuracyInfoMap_Output contains predicted value out of range.")
				}

				if (predictedValue < rowValueRangeStart){
					continue
				}
				if (rowIndex != finalIndex){

					nextRowRangeStartValue := valueRowsSlice[rowIndex+1]

					if (predictedValue >= nextRowRangeStartValue){
						continue
					}
				}

				percentageOfLociTested := attributePredictionInfo.PercentageOfLociTested

				if (percentageOfLociTested <= 33){
					predictionAccuracyDistances_Output_0to33 = append(predictionAccuracyDistances_Output_0to33, attributePredictionAccuracyDistancesList...)

				} else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){

					predictionAccuracyDistances_Output_34to66 = append(predictionAccuracyDistances_Output_34to66, attributePredictionAccuracyDistancesList...)

				} else {

					predictionAccuracyDistances_Output_67to100 = append(predictionAccuracyDistances_Output_67to100, attributePredictionAccuracyDistancesList...)
				}

				existingList, exists := newChart2LineDistancesMap_Output[percentageOfLociTested]
				if (exists == false){
					newList := slices.Clone(attributePredictionAccuracyDistancesList)
					newChart2LineDistancesMap_Output[percentageOfLociTested] = newList
				} else {
					existingList = append(existingList, attributePredictionAccuracyDistancesList...)
					newChart2LineDistancesMap_Output[percentageOfLociTested] = existingList
				}
			}

			// We create the new chart2LineInfo items and add them to the chart2LineInfos List

			newChart2LineInfo_Input := Chart2LineInfo{
				Name: inputOrOutputValueRangeFormatted + " (Input)",
				PredictionDistancesMap: newChart2LineDistancesMap_Input,
			}

			chart2LineInfosList = append(chart2LineInfosList, newChart2LineInfo_Input)

			newChart2LineInfo_Output := Chart2LineInfo{
				Name: inputOrOutputValueRangeFormatted + " (Output)",
				PredictionDistancesMap: newChart2LineDistancesMap_Output,
			}

			chart2LineInfosList = append(chart2LineInfosList, newChart2LineInfo_Output)

			// Now we calculate the average distance for predictions within the row's range

			distanceLabel_Input_0to33, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Input_0to33)
			if (err != nil) { return nil, err }
		
			distanceLabel_Input_34to66, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Input_34to66)
			if (err != nil) { return nil, err }

			distanceLabel_Input_67to100, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Input_67to100)
			if (err != nil) { return nil, err }

			distanceLabel_Output_0to33, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Output_0to33)
			if (err != nil) { return nil, err }
		
			distanceLabel_Output_34to66, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Output_34to66)
			if (err != nil) { return nil, err }

			distanceLabel_Output_67to100, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Output_67to100)
			if (err != nil) { return nil, err }

			averageDistanceLabel_0to33 := container.NewHBox(layout.NewSpacer(), distanceLabel_Input_0to33, getBoldLabel("/"), distanceLabel_Output_0to33, layout.NewSpacer())
			averageDistanceLabel_34to66 := container.NewHBox(layout.NewSpacer(), distanceLabel_Input_34to66, getBoldLabel("/"), distanceLabel_Output_34to66, layout.NewSpacer())
			averageDistanceLabel_67to100 := container.NewHBox(layout.NewSpacer(), distanceLabel_Input_67to100, getBoldLabel("/"), distanceLabel_Output_67to100, layout.NewSpacer())

			inputOrOutputValueColumn.Add(inputOrOutputValueRangeLabel)
			accuracyRangesColumn_0to33.Add(averageDistanceLabel_0to33)
			accuracyRangesColumn_34to66.Add(averageDistanceLabel_34to66)
			accuracyRangesColumn_67to100.Add(averageDistanceLabel_67to100)

			inputOrOutputValueColumn.Add(widget.NewSeparator())
			accuracyRangesColumn_0to33.Add(widget.NewSeparator())
			accuracyRangesColumn_34to66.Add(widget.NewSeparator())
			accuracyRangesColumn_67to100.Add(widget.NewSeparator())
		}

		// We add one final row: The average row
		// This row provides the accuracy for all inputs/outputs, not split by the value of the inputs/outputs
		// The intput and the output result should be the same, so we only have to calculate for one of the two.

		predictionAccuracyDistances_Input_0to33 := make([]float64, 0)
		predictionAccuracyDistances_Input_34to66 := make([]float64, 0)
		predictionAccuracyDistances_Input_67to100 := make([]float64, 0)

		for attributePredictionInfo, attributePredictionAccuracyDistancesList := range predictionAccuracyInfoMap_Input{

			percentageOfLociTested := attributePredictionInfo.PercentageOfLociTested

			if (percentageOfLociTested <= 33){
				predictionAccuracyDistances_Input_0to33 = append(predictionAccuracyDistances_Input_0to33, attributePredictionAccuracyDistancesList...)

			} else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){

				predictionAccuracyDistances_Input_34to66 = append(predictionAccuracyDistances_Input_34to66, attributePredictionAccuracyDistancesList...)

			} else {

				predictionAccuracyDistances_Input_67to100 = append(predictionAccuracyDistances_Input_67to100, attributePredictionAccuracyDistancesList...)
			}
		}

		inputOrOutputValueRangeLabel := getBoldLabel("All Inputs/Outputs")

		distanceLabel_Input_0to33, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Input_0to33)
		if (err != nil) { return nil, err }

		distanceLabel_Input_34to66, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Input_34to66)
		if (err != nil) { return nil, err }

		distanceLabel_Input_67to100, err := getRequiredDistanceLabelFromDistancesSlice(predictionAccuracyDistances_Input_67to100)
		if (err != nil) { return nil, err }

		averageDistanceLabel_0to33 := getWidgetCentered(distanceLabel_Input_0to33)
		averageDistanceLabel_34to66 := getWidgetCentered(distanceLabel_Input_34to66)
		averageDistanceLabel_67to100 := getWidgetCentered(distanceLabel_Input_67to100)

		inputOrOutputValueColumn.Add(inputOrOutputValueRangeLabel)
		accuracyRangesColumn_0to33.Add(averageDistanceLabel_0to33)
		accuracyRangesColumn_34to66.Add(averageDistanceLabel_34to66)
		accuracyRangesColumn_67to100.Add(averageDistanceLabel_67to100)

		resultsGrid := container.NewHBox(layout.NewSpacer(), inputOrOutputValueColumn, accuracyRangesColumn_0to33, accuracyRangesColumn_34to66, accuracyRangesColumn_67to100, layout.NewSpacer())

		return resultsGrid, nil
	}

	resultsGrid, err := getResultsGrid()
	if (err != nil){
		setErrorEncounteredPage(window, err, func(){setHomePage(window)})
		return
	}

	page := container.NewVBox(title, exitButton, widget.NewSeparator(), description1, widget.NewSeparator(), attributeNameRow, widget.NewSeparator(), description2, description3, description4, description5, description6, description7, widget.NewSeparator(), accuracyRangeTitle, accuracySelectorCentered, widget.NewSeparator(), viewChartButtonsGridCentered, widget.NewSeparator(), resultsGrid)

	pageScrollable := container.NewVScroll(page)

	window.SetContent(pageScrollable)
}


func setViewChartImagePage(window fyne.Window, chartImage image.Image, previousPage func()){

	title := getBoldLabelCentered("Viewing Chart")

	backButton := getBackButtonCentered(previousPage)

	header := container.NewVBox(title, backButton, widget.NewSeparator())

	//TODO: Add a right-click to save ability

	fyneImage := canvas.NewImageFromImage(chartImage)
	fyneImage.FillMode = canvas.ImageFillContain

	page := container.NewBorder(header, nil, nil, nil, fyneImage)

	window.SetContent(page)
}


// This function returns a list of training data and testing data filepaths for an attribute.
//Outputs:
//	-[]string: Sorted list of training data filepaths
//	-[]string: Unsorted list of testing data filepaths
//	-error
func getTrainingAndTestingDataFilepathLists(attributeName string)([]string, []string, error){

	attributeNameWithoutWhitespaces := strings.ReplaceAll(attributeName, " ", "")

	trainingDataFolderpath := goFilepath.Join("./TrainingData/", attributeNameWithoutWhitespaces)

	filesList, err := os.ReadDir(trainingDataFolderpath)
	if (err != nil) { return nil, nil, err }

	// This map stores the file name for each training data
	trainingDataFilenamesMap := make(map[string]struct{})

	for _, filesystemObject := range filesList{

		filepathIsFolder := filesystemObject.IsDir()
		if (filepathIsFolder == true){
			// Folder is corrupt
			return nil, nil, errors.New("Training data is corrupt for attribute: " + attributeName)
		}

		fileName := filesystemObject.Name()

		trainingDataFilenamesMap[fileName] = struct{}{}
	}

	numberOfTrainingDataFiles := len(trainingDataFilenamesMap)

	if (numberOfTrainingDataFiles == 0){
		return nil, nil, errors.New("No training data exists for attribute: " + attributeName)
	}

	getNumberOfExpectedTrainingDatas := func()(int, error){

		switch attributeName{

			case "Eye Color":{
				return 468395, nil
			}
			case "Lactose Tolerance":{
				return 2478, nil
			}
			case "Height":{
				return 152095, nil
			}
			case "Autism":{
				return 93895, nil
			}
			case "Homosexualness":{
				return 317, nil
			}
			case "Obesity":{
				return 7311, nil
			}
		}

		return 0, errors.New("Unknown attributeName: " + attributeName)
	}

	numberOfExpectedTrainingDatas, err := getNumberOfExpectedTrainingDatas()
	if (err != nil){ return nil, nil, err }

	if (numberOfTrainingDataFiles != numberOfExpectedTrainingDatas){

		numberOfTrainingDataFilesString := helpers.ConvertIntToString(numberOfTrainingDataFiles)

		return nil, nil, errors.New(attributeName + " quantity of training datas is unexpected: " + numberOfTrainingDataFilesString)
	}

	// We sort the training data to be in a deterministically random order
	// This allows us to train the neural network in the same order each time
	// We do this so we can generate deterministic models which are identical byte-for-byte

	// We have to set aside 200 person's training datas for testing the neural network
	//
	// We have to remove them per-person because each person has <=200 training datas.
	// Otherwise, we would be training and testing on data from the same people.
	// We need to test with people that the models were never trained upon.

	// First we extract the person identifiers from the data

	personIdentifiersMap := make(map[int]struct{})

	for trainingDataFilename, _ := range trainingDataFilenamesMap{

		// Example filepath format: "Person4680_TrainingData_89.gob"

		trimmedFilename := strings.TrimPrefix(trainingDataFilename, "Person")

		personIdentifierString, _, underscoreExists := strings.Cut(trimmedFilename, "_")
		if (underscoreExists == false){
			return nil, nil, errors.New("Invalid trainingData filename: " + trainingDataFilename)	
		}

		personIdentifier, err := helpers.ConvertStringToInt(personIdentifierString)
		if (err != nil){
			return nil, nil, errors.New("Invalid trainingData filename: " + trainingDataFilename)	
		}

		personIdentifiersMap[personIdentifier] = struct{}{}
	}

	personIdentifiersList := helpers.GetListOfMapKeys(personIdentifiersMap)

	// We sort the person identifiers list in ascending order

	slices.Sort(personIdentifiersList)

	// Now we deterministically randomize the order of the person identifiers list
	pseudorandomNumberGenerator := mathRand.New(mathRand.NewPCG(1, 2))

	pseudorandomNumberGenerator.Shuffle(len(personIdentifiersList), func(i int, j int){
		personIdentifiersList[i], personIdentifiersList[j] = personIdentifiersList[j], personIdentifiersList[i]
	})

	trainingSetFilepathsList := make([]string, 0)
	testingSetFilepathsList := make([]string, 0)

	quantityOfUsers := len(personIdentifiersList)

	if (quantityOfUsers < 275){
		quantityOfUsersString := helpers.ConvertIntToString(quantityOfUsers)
		return nil, nil, errors.New("Too few training data users for attribute: " + attributeName + ": " + quantityOfUsersString)
	}

	// We use 150 people for testing (validation), so we don't train using them
	numberOfTrainingUsers := quantityOfUsers - 150

	for personIndex, personIdentifier := range personIdentifiersList{

		// Example filepath format: "Person4680_TrainingData_89.gob"

		personIdentifierString := helpers.ConvertIntToString(personIdentifier)

		trainingDataFilenamePrefix := "Person" + personIdentifierString + "_TrainingData_"

		for k:=1; k <= 300; k++{

			kString := helpers.ConvertIntToString(k)

			trainingDataFilename := trainingDataFilenamePrefix + kString + ".gob"

			_, fileExists := trainingDataFilenamesMap[trainingDataFilename]
			if (fileExists == false){
				// Some training datas don't exist due to how training datas are randomly created
				// Sometimes, no alleles exist, so we skip creating the training data
				// Sometimes, the created training data is a duplicate, so we skip it
				continue
			}

			trainingDataFilepath := goFilepath.Join(trainingDataFolderpath, trainingDataFilename)

			if (personIndex < numberOfTrainingUsers){
				trainingSetFilepathsList = append(trainingSetFilepathsList, trainingDataFilepath)
			} else {
				testingSetFilepathsList = append(testingSetFilepathsList, trainingDataFilepath)
			}
		}
	}

	return trainingSetFilepathsList, testingSetFilepathsList, nil
}

// We use this to define a custom fyne theme
// We are only overriding the foreground color to pure black
type customTheme struct{
	defaultTheme fyne.Theme
}

func getCustomFyneTheme()fyne.Theme{

	standardThemeObject := theme.LightTheme()

	newTheme := customTheme{
		defaultTheme: standardThemeObject,
	}

	return newTheme
}


// This function is used to define our custom fyne themes
// It changes a few default colors, while leaving all other colors the same as the default theme
func (input customTheme)Color(colorName fyne.ThemeColorName, variant fyne.ThemeVariant)color.Color{

	switch colorName{

		case theme.ColorNameForeground:{

			newColor := color.Black
			return newColor
		}
		case theme.ColorNameSeparator:{

			// This is the color used for separators

			newColor := color.Black
			return newColor
		}
		case theme.ColorNameInputBackground:{

			// This color is used for the background of input elements such as text entries
			newColor, err := imagery.GetColorObjectFromColorCode("b3b3b3")
			if (err == nil){
				return newColor
			}
		}
		case theme.ColorNameButton:{

			// This is the color used for buttons
			newColor, err := imagery.GetColorObjectFromColorCode("d8d8d8")
			if (err == nil){
				return newColor
			}
		}
		case theme.ColorNamePlaceHolder:{

			// This is the color used for text

			newColor, err := imagery.GetColorObjectFromColorCode("4d4d4d")
			if (err == nil){
				return newColor
			}
		}

	}

	// We will use the default color for this theme
	return input.defaultTheme.Color(colorName, variant)
}

// Our custom themes change nothing about the default theme fonts
func (input customTheme)Font(style fyne.TextStyle)fyne.Resource{

	themeFont := input.defaultTheme.Font(style)

	return themeFont
}

// Our custom themes change nothing about the default theme icons
func (input customTheme)Icon(iconName fyne.ThemeIconName)fyne.Resource{

	themeIcon := input.defaultTheme.Icon(iconName)

	return themeIcon
}

func (input customTheme)Size(name fyne.ThemeSizeName)float32{

	themeSize := input.defaultTheme.Size(name)

	if (name == theme.SizeNameText){

		// After fyne v2.3.0, text labels are no longer the same height as buttons
		// We increase the text size so that a text label is the same height as a button
		// We need to increase text size because we are creating grids by creating multiple VBoxes, and connecting them with an HBox
		//
		// If we could create grids in a different way, we could avoid having to do this
		// Example: Create a new grid type: container.NewThinGrid?
		//		-The columns will only be as wide as the the widest element within them
		//		-We can add separators between each row (grid.ShowRowLines = true) or between columns (grid.ShowColumnLines = true)
		//		-We can add borders (grid.ShowTopBorder = true, grid.ShowBottomBorder = true, grid.ShowLeftBorder = true, grid.ShowRightBorder = true)

		// Using a different grid type is the solution we need to eventually use
		// Then, we can show the user an option to increase the text size globally, and all grids will still render correctly

		result := themeSize * 1.08

		return result
	}

	return themeSize
}



