diff --git a/Changelog.md b/Changelog.md index dd2cc85..c44906f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ Small and insignificant changes may not be included in this log. ## Unversioned Changes +* Added numeric traits to genetic analyses. - *Simon Sarasova* * Improved Documentation.md and Future-Plans.md. - *Simon Sarasova* * Improved Future-Plans.md. - *Simon Sarasova* * Added Merkle Tree Payment Proofs to Future-Plans.md. - *Simon Sarasova* diff --git a/Contributors.md b/Contributors.md index 8a1ce86..1a365bb 100644 --- a/Contributors.md +++ b/Contributors.md @@ -9,4 +9,4 @@ Many other people have written code for modules which are imported by Seekia. Th Name | Date Of First Commit | Number Of Commits --- | --- | --- -Simon Sarasova | June 13, 2023 | 274 \ No newline at end of file +Simon Sarasova | June 13, 2023 | 275 \ No newline at end of file diff --git a/gui/viewAnalysisGui_Couple.go b/gui/viewAnalysisGui_Couple.go index 07a59db..daa2142 100644 --- a/gui/viewAnalysisGui_Couple.go +++ b/gui/viewAnalysisGui_Couple.go @@ -93,8 +93,7 @@ func setViewCoupleGeneticAnalysisPage(window fyne.Window, person1Identifier stri setViewCoupleGeneticAnalysisDiscreteTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, currentPage) }) numericTraitsButton := widget.NewButton("Numeric Traits", func(){ - //TODO - showUnderConstructionDialog(window) + setViewCoupleGeneticAnalysisNumericTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, currentPage) }) categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, discreteTraitsButton, numericTraitsButton)) @@ -1789,28 +1788,11 @@ func setViewPolygenicDiseaseSampleOffspringRiskScoresChart(window fyne.Window, d getOffspringSampleRiskScoresChartImage := func()(image.Image, error){ - // Map Structure: Risk Score -> Number of offspring with that risk score - offspringRiskScoreCountsMap := make(map[int]int) - - for _, offspringRiskScore := range sampleOffspringRiskScoresList{ - offspringRiskScoreCountsMap[offspringRiskScore] += 1 - } - - //TODO: Move StatisticsDatum to its own package, because we are using it for non-user purposes, and will continue to do so offspringStatisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0) for riskScore:=0; riskScore <= 10; riskScore += 1{ - getOffspringCount := func()int{ - - offspringCount, exists := offspringRiskScoreCountsMap[riskScore] - if (exists == false){ - return 0 - } - return offspringCount - } - - offspringCount := getOffspringCount() + offspringCount := helpers.CountMatchingElementsInSlice(sampleOffspringRiskScoresList, riskScore) riskScoreString := helpers.ConvertIntToString(riskScore) offspringCountString := helpers.ConvertIntToString(offspringCount) @@ -1917,9 +1899,8 @@ func setViewCoupleGeneticAnalysisDiscreteTraitsPage(window fyne.Window, person1N traitRulesList := traitObject.RulesList if (len(traitLociList) == 0 && len(traitRulesList) == 0){ - // This trait does not have any rules + // This trait does not have any loci or rules // We cannot analyze it yet - // We will add neural network prediction so we can predict these traits continue } @@ -2133,6 +2114,18 @@ func setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage(window fyne.Window, pe }) traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitNameInfoButton, layout.NewSpacer()) + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ + setErrorEncounteredPage(window, errors.New("setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage called with non-discrete trait: " + traitName), previousPage) + return + } + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) emptyLabel1 := widget.NewLabel("") @@ -2183,14 +2176,6 @@ func setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage(window fyne.Window, pe pairNameColumn.Add(genomePairNameLabel) viewDetailsButtonsColumn.Add(viewAnalysisDetailsButton) - traitObject, err := traits.GetTraitObject(traitName) - if (err != nil) { return err } - - traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric - if (traitIsDiscreteOrNumeric != "Discrete"){ - return errors.New("setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage called with non-discrete trait: " + traitName) - } - neuralNetworkExists, neuralNetworkAnalysisExists, offspringOutcomeProbabilitiesMap_NeuralNetwork, neuralNetworkPredictionConfidence, _, _, anyRulesExist, rulesAnalysisExists, offspringOutcomeProbabilitiesMap_Rules, _, quantityOfRulesTested, _, _, err := readGeneticAnalysis.GetOffspringDiscreteTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) if (err != nil) { return err } if (neuralNetworkExists == false && anyRulesExist == false){ @@ -2951,5 +2936,637 @@ func setViewCoupleGeneticAnalysisDiscreteTraitRuleDetailsPage(window fyne.Window } +func setViewCoupleGeneticAnalysisNumericTraitsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisNumericTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Numeric Traits") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the average numeric trait outcomes for the couple's offspring.") + + getTraitsGrid := func()(*fyne.Container, error){ + + pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier, secondGenomePairExists, _, _, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisObject) + if (err != nil){ return nil, err } + + mainGenomePairIdentifier := helpers.JoinTwo16ByteArrays(pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier) + + emptyLabel1 := widget.NewLabel("") + traitNameLabel := getItalicLabelCentered("Trait Name") + + emptyLabel2 := widget.NewLabel("") + predictedOutcomeLabel := getItalicLabelCentered("Predicted Outcome") + + quantityOfLabel := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + emptyLabel3 := widget.NewLabel("") + confidenceRangeLabel := getItalicLabelCentered("Confidence Range") + + emptyLabel4 := widget.NewLabel("") + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabel5 := widget.NewLabel("") + emptyLabel6 := widget.NewLabel("") + + traitNameColumn := container.NewVBox(emptyLabel1, traitNameLabel, widget.NewSeparator()) + predictedOutcomeColumn := container.NewVBox(emptyLabel2, predictedOutcomeLabel, widget.NewSeparator()) + confidenceRangeColumn := container.NewVBox(emptyLabel3, confidenceRangeLabel, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel, lociKnownLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(emptyLabel4, conflictExistsLabel, widget.NewSeparator()) + viewDetailsButtonsColumn := container.NewVBox(emptyLabel5, emptyLabel6, widget.NewSeparator()) + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + continue + } + + traitLociList := traitObject.LociList + + if (len(traitLociList) == 0){ + // This trait does not have any loci + // We cannot analyze it yet + continue + } + + traitName := traitObject.TraitName + + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (neuralNetworkExists == false){ + // We cannot analyze this trait + continue + } + + traitNameLabel := getBoldLabelCentered(traitName) + + analysisExists, offspringAverageOutcome, predictionConfidenceRangesMap, quantityOfLociKnown, _, _, conflictExists, err := readGeneticAnalysis.GetOffspringNumericTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, mainGenomePairIdentifier) + if (err != nil) { return nil, err } + + getPredictedOutcomeLabel := func()fyne.Widget{ + if (analysisExists == false){ + result := widget.NewLabel("Unknown") + return result + } + + predictedOutcomeString := helpers.ConvertFloat64ToStringRounded(offspringAverageOutcome, 2) + + predictedOutcomeFormatted := predictedOutcomeString + " centimeters" + + outcomeLabel := getBoldLabel(predictedOutcomeFormatted) + + return outcomeLabel + } + + predictedOutcomeLabel := getPredictedOutcomeLabel() + + predictedOutcomeLabelCentered := getWidgetCentered(predictedOutcomeLabel) + + getConfidenceRangeLabel := func()(fyne.Widget, error){ + if (analysisExists == false){ + + result := widget.NewLabel("Unknown") + return result, nil + } + + // This is a list of the percentage accuracies in the map + // For example: 80% == The distance from the prediction you must travel for 80% of the predictions to be + // accurate within that range + confidenceRangePercentagesList := helpers.GetListOfMapKeys(predictionConfidenceRangesMap) + + // We sort the list so the percentage is always the same upon refreshing the page + slices.Sort(confidenceRangePercentagesList) + + closestToEightyPercentage, err := helpers.GetClosestIntInList(confidenceRangePercentagesList, 80) + if (err != nil) { return nil, err } + + closestToEightyPercentageConfidenceDistance, exists := predictionConfidenceRangesMap[closestToEightyPercentage] + if (exists == false){ + return nil, errors.New("GetListOfMapKeys returning list of elements which contains element which is not in the map.") + } + + closestConfidenceDistanceString := helpers.ConvertFloat64ToStringRounded(closestToEightyPercentageConfidenceDistance, 2) + + closestToEightyPercentageString := helpers.ConvertIntToString(closestToEightyPercentage) + + confidenceRangeLabelValueFormatted := "+/- " + closestConfidenceDistanceString + " (" + closestToEightyPercentageString + "% Confidence)" + + confidenceRangeLabel := getBoldLabel(confidenceRangeLabelValueFormatted) + + return confidenceRangeLabel, nil + } + + confidenceRangeLabel, err := getConfidenceRangeLabel() + if (err != nil) { return nil, err } + + confidenceRangeLabelCentered := getWidgetCentered(confidenceRangeLabel) + + totalQuantityOfLoci := len(traitLociList) + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisNumericTraitDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, currentPage) + })) + + traitNameColumn.Add(traitNameLabel) + predictedOutcomeColumn.Add(predictedOutcomeLabelCentered) + confidenceRangeColumn.Add(confidenceRangeLabelCentered) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + conflictExistsColumn.Add(conflictExistsLabel) + viewDetailsButtonsColumn.Add(viewDetailsButton) + + traitNameColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + confidenceRangeColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewDetailsButtonsColumn.Add(widget.NewSeparator()) + } + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + predictedOutcomeColumn.Add(predictedOutcomeHelpButton) + + confidenceRangeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + confidenceRangeColumn.Add(confidenceRangeHelpButton) + + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) + + traitsGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, predictedOutcomeColumn, confidenceRangeColumn, quantityOfLociKnownColumn) + + if (secondGenomePairExists == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + conflictExistsColumn.Add(conflictExistsHelpButton) + + traitsGrid.Add(conflictExistsColumn) + } + + traitsGrid.Add(viewDetailsButtonsColumn) + traitsGrid.Add(layout.NewSpacer()) + + return traitsGrid, nil + } + + traitsGrid, err := getTraitsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), traitsGrid) + + setPageContent(page, window) +} +func setViewCoupleGeneticAnalysisNumericTraitDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisNumericTraitDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Analysis - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier, secondGenomePairExists, pair2Person1GenomeIdentifier, pair2Person2GenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisObject) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (secondGenomePairExists == false){ + description := getLabelCentered("Below is the trait analysis for the couple's offspring.") + + return description + } + + description1 := getLabelCentered("Below is the trait analysis for the couple's offspring.") + description2 := getLabelCentered("Each genome pair combines different genomes from each person.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + traitNameLabel := widget.NewLabel("Trait:") + traitNameText := getBoldLabel(traitName) + traitNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitDetailsPage(window, traitName, currentPage) + }) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitNameInfoButton, layout.NewSpacer()) + + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (neuralNetworkExists == false){ + // We cannot analyze this trait + setErrorEncounteredPage(window, errors.New("setViewCoupleGeneticAnalysisNumericTraitDetailsPage called non-analyzable trait: " + traitName), previousPage) + return + } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + setErrorEncounteredPage(window, errors.New("setViewCoupleGeneticAnalysisNumericTraitDetailsPage called with non-numeric trait: " + traitName), previousPage) + return + } + + traitLociList := traitObject.LociList + + emptyLabel1 := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") + + emptyLabel3 := widget.NewLabel("") + genomePairLabel := getItalicLabelCentered("Genome Pair") + + emptyLabel4 := widget.NewLabel("") + predictedOutcomeLabel := getItalicLabelCentered("Predicted Outcome") + + emptyLabel5 := widget.NewLabel("") + confidenceRangeLabel := getItalicLabelCentered("Confidence Range") + + quantityOfLabel := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + emptyLabel6 := widget.NewLabel("") + emptyLabel7 := widget.NewLabel("") + + emptyLabel8 := widget.NewLabel("") + emptyLabel9 := widget.NewLabel("") + + viewGenomePairButtonsColumn := container.NewVBox(emptyLabel1, emptyLabel2, widget.NewSeparator()) + pairNameColumn := container.NewVBox(emptyLabel3, genomePairLabel, widget.NewSeparator()) + predictedOutcomeColumn := container.NewVBox(emptyLabel4, predictedOutcomeLabel, widget.NewSeparator()) + predictionConfidenceRangeColumn := container.NewVBox(emptyLabel5, confidenceRangeLabel, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel, lociKnownLabel, widget.NewSeparator()) + viewSampleOffspringButtonsColumn := container.NewVBox(emptyLabel6, emptyLabel7, widget.NewSeparator()) + viewDetailsButtonsColumn := container.NewVBox(emptyLabel8, emptyLabel9, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ + + genomePairIdentifier := helpers.JoinTwo16ByteArrays(person1GenomeIdentifier, person2GenomeIdentifier) + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + viewGenomePairButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + analysisExists, offspringAverageOutcome, predictionConfidenceRangesMap, quantityOfLociKnown, _, sampleOffspringOutcomesList, _, err := readGeneticAnalysis.GetOffspringNumericTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) + if (err != nil) { return err } + + getPredictedOutcomeLabel := func()fyne.Widget{ + if (analysisExists == false){ + unknownLabel := widget.NewLabel("Unknown") + return unknownLabel + } + + offspringAverageOutcomeString := helpers.ConvertFloat64ToStringRounded(offspringAverageOutcome, 2) + + offspringAverageOutcomeFormatted := offspringAverageOutcomeString + " centimeters" + + predictedOutcomeLabel := getBoldLabel(offspringAverageOutcomeFormatted) + + return predictedOutcomeLabel + } + + predictedOutcomeLabel := getPredictedOutcomeLabel() + + predictedOutcomeLabelCentered := getWidgetCentered(predictedOutcomeLabel) + + getConfidenceRangeLabel := func()(fyne.Widget, error){ + if (analysisExists == false){ + + result := widget.NewLabel("Unknown") + return result, nil + } + + // This is a list of the percentage accuracies in the map + // For example: 80% == The distance from the prediction you must travel for 80% of the predictions to be + // accurate within that range + confidenceRangePercentagesList := helpers.GetListOfMapKeys(predictionConfidenceRangesMap) + + // We sort the list so the percentage is always the same upon refreshing the page + slices.Sort(confidenceRangePercentagesList) + + closestToEightyPercentage, err := helpers.GetClosestIntInList(confidenceRangePercentagesList, 80) + if (err != nil) { return nil, err } + + closestToEightyPercentageConfidenceDistance, exists := predictionConfidenceRangesMap[closestToEightyPercentage] + if (exists == false){ + return nil, errors.New("GetListOfMapKeys returning list of elements which contains element which is not in the map.") + } + + closestConfidenceDistanceString := helpers.ConvertFloat64ToStringRounded(closestToEightyPercentageConfidenceDistance, 2) + + closestToEightyPercentageString := helpers.ConvertIntToString(closestToEightyPercentage) + + confidenceRangeLabelValueFormatted := "+/- " + closestConfidenceDistanceString + " (" + closestToEightyPercentageString + "% Confidence)" + + confidenceRangeLabel := getBoldLabel(confidenceRangeLabelValueFormatted) + + return confidenceRangeLabel, nil + } + + confidenceRangeLabel, err := getConfidenceRangeLabel() + if (err != nil) { return err } + + confidenceRangeLabelCentered := getWidgetCentered(confidenceRangeLabel) + + totalQuantityOfLoci := len(traitLociList) + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + viewSampleOffspringsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewNumericTraitSampleOffspringRiskScoresChart(window, traitName, sampleOffspringOutcomesList, quantityOfLociKnown, currentPage) + }) + + viewAnalysisDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + viewGenomePairButtonsColumn.Add(viewGenomePairButton) + pairNameColumn.Add(genomePairNameLabel) + predictedOutcomeColumn.Add(predictedOutcomeLabelCentered) + predictionConfidenceRangeColumn.Add(confidenceRangeLabelCentered) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + viewSampleOffspringButtonsColumn.Add(viewSampleOffspringsButton) + viewDetailsButtonsColumn.Add(viewAnalysisDetailsButton) + + viewGenomePairButtonsColumn.Add(widget.NewSeparator()) + pairNameColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + predictionConfidenceRangeColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) + viewSampleOffspringButtonsColumn.Add(widget.NewSeparator()) + viewDetailsButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + err = addGenomePairRow("Pair 1", pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (secondGenomePairExists == true){ + err := addGenomePairRow("Pair 2", pair2Person1GenomeIdentifier, pair2Person2GenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + predictedOutcomeColumn.Add(predictedOutcomeHelpButton) + + confidenceRangeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + predictionConfidenceRangeColumn.Add(confidenceRangeHelpButton) + + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, predictedOutcomeColumn, predictionConfidenceRangeColumn, quantityOfLociKnownColumn, viewSampleOffspringButtonsColumn, viewDetailsButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + +// This is a page that shows the user 100 sample offspring trait outcomes on a bar chart +// This helps users to visualize the standard deviation of their offspring's trait outcomes with this user +func setViewNumericTraitSampleOffspringRiskScoresChart(window fyne.Window, traitName string, sampleOffspringOutcomesList []float64, quantityOfLociKnown int, previousPage func()){ + + currentPage := func(){setViewNumericTraitSampleOffspringRiskScoresChart(window, traitName, sampleOffspringOutcomesList, quantityOfLociKnown, previousPage)} + + title := getPageTitleCentered("Viewing Sample Offspring Outcomes Chart") + + backButton := getBackButtonCentered(previousPage) + + description := widget.NewLabel("Below is a chart of 100 sample offspring trait outcomes for this disease.") + descriptionHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + descriptionRow := container.NewHBox(layout.NewSpacer(), description, descriptionHelpButton, layout.NewSpacer()) + + traitNameTitle := widget.NewLabel("Trait Name:") + traitNameLabel := getBoldLabel(traitName) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameTitle, traitNameLabel, layout.NewSpacer()) + + if (len(sampleOffspringOutcomesList) == 0){ + description2 := getBoldLabelCentered("There is no offspring information available for this trait.") + description3 := getBoldLabelCentered("This is because there were no trait loci for which both prospective parents had information.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), description2, description3) + + setPageContent(page, window) + return + } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + traitLociList := traitObject.LociList + + totalNumberOfLoci := len(traitLociList) + totalNumberOfLociString := helpers.ConvertIntToString(totalNumberOfLoci) + + numberOfLociKnownTitle := widget.NewLabel("Quantity Of Loci Known:") + numberOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + numberOfLociKnownLabel := getBoldLabel(numberOfLociKnownString + "/" + totalNumberOfLociString) + + lociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + quantityOfLociKnownRow := container.NewHBox(layout.NewSpacer(), numberOfLociKnownTitle, numberOfLociKnownLabel, lociKnownHelpButton, layout.NewSpacer()) + + getOffspringSampleOutcomesChartImage := func()(image.Image, error){ + + if (len(sampleOffspringOutcomesList) != 100){ + return nil, errors.New("setViewNumericTraitSampleOffspringRiskScoresChart called with offspringOutcomesList that is not 100 items in length.") + } + + // We sort the list in ascending order + slices.Sort(sampleOffspringOutcomesList) + + getOffspringStatisticsDatumsList := func()([]statisticsDatum.StatisticsDatum){ + + offspringStatisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0) + + smallestItem := sampleOffspringOutcomesList[0] + largestItem := sampleOffspringOutcomesList[99] + + if (smallestItem == largestItem){ + + // We can't split the values into groups + // Every offspring has the same value + + onlyValueString := helpers.ConvertFloat64ToStringRounded(smallestItem, 2) + + onlyValueFormatted := onlyValueString + " centimeters" + + newStatisticsDatum := statisticsDatum.StatisticsDatum{ + Label: onlyValueString, + LabelFormatted: onlyValueFormatted, + Value: float64(100), + ValueFormatted: "100", + } + + offspringStatisticsDatumsList = append(offspringStatisticsDatumsList, newStatisticsDatum) + + return offspringStatisticsDatumsList + } + + // We split all outcomes into ten groups + + sizeOfEachGroup := (largestItem-smallestItem)/10 + + index := float64(0) + + for { + + getGroupUpperBound := func()float64{ + + if (len(offspringStatisticsDatumsList) == 9){ + // We are adding the last datum + return largestItem + } + + groupUpperBound := index + sizeOfEachGroup + + return groupUpperBound + } + + groupUpperBound := getGroupUpperBound() + + offspringInGroupCount := 0 + + for _, outcomeValue := range sampleOffspringOutcomesList{ + + if (outcomeValue >= index && outcomeValue < groupUpperBound){ + offspringInGroupCount += 1 + } + } + + groupLowerBoundString := helpers.ConvertFloat64ToStringRounded(index, 1) + groupUpperBoundString := helpers.ConvertFloat64ToStringRounded(groupUpperBound, 1) + + groupDescription := groupLowerBoundString + "-" + groupUpperBoundString + " centimeters" + + offspringCountString := helpers.ConvertIntToString(offspringInGroupCount) + + newStatisticsDatum := statisticsDatum.StatisticsDatum{ + Label: groupDescription, + LabelFormatted: groupDescription, + Value: float64(offspringInGroupCount), + ValueFormatted: offspringCountString, + } + + offspringStatisticsDatumsList = append(offspringStatisticsDatumsList, newStatisticsDatum) + + if (len(offspringStatisticsDatumsList) == 10){ + break + } + + index += sizeOfEachGroup + } + + return offspringStatisticsDatumsList + } + + offspringStatisticsDatumsList := getOffspringStatisticsDatumsList() + + chartTitle := traitName + ": 100 Prospective Offspring Values" + + formatYAxisValuesFunction := func(inputTraitValue float64)(string, error){ + + result := helpers.ConvertFloat64ToStringRounded(inputTraitValue, 2) + + return result, nil + } + + offspringsChart, err := createCharts.CreateBarChart(chartTitle, offspringStatisticsDatumsList, formatYAxisValuesFunction, true, " Offspring") + if (err != nil) { return nil, err } + + return offspringsChart, nil + } + + offspringOutcomesChartImage, err := getOffspringSampleOutcomesChartImage() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewChartFullscreenButton := getWidgetCentered(widget.NewButtonWithIcon("View Fullscreen", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, offspringOutcomesChartImage, currentPage) + })) + + pageHeader := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), quantityOfLociKnownRow, widget.NewSeparator()) + + chartFyneImage := canvas.NewImageFromImage(offspringOutcomesChartImage) + chartFyneImage.FillMode = canvas.ImageFillContain + + page := container.NewBorder(pageHeader, viewChartFullscreenButton, nil, nil, chartFyneImage) + + setPageContent(page, window) +} + diff --git a/gui/viewAnalysisGui_Person.go b/gui/viewAnalysisGui_Person.go index d3f3fc5..cf1af7c 100644 --- a/gui/viewAnalysisGui_Person.go +++ b/gui/viewAnalysisGui_Person.go @@ -26,8 +26,10 @@ import "seekia/internal/genetics/myPeople" import "seekia/internal/genetics/readGeneticAnalysis" import "seekia/internal/helpers" +import "slices" import "errors" + func setViewPersonGeneticAnalysisPage(window fyne.Window, personIdentifier string, analysisObject geneticAnalysis.PersonAnalysis, numberOfGenomesAnalyzed int, previousPage func()){ appMemory.SetMemoryEntry("CurrentViewedPage", "ViewGeneticAnalysisPage") @@ -75,8 +77,7 @@ func setViewPersonGeneticAnalysisPage(window fyne.Window, personIdentifier strin setViewPersonGeneticAnalysisDiscreteTraitsPage(window, personIdentifier, analysisObject, currentPage) }) numericTraitsButton := widget.NewButton("Numeric Traits", func(){ - //TODO - showUnderConstructionDialog(window) + setViewPersonGeneticAnalysisNumericTraitsPage(window, personIdentifier, analysisObject, currentPage) }) categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, discreteTraitsButton, numericTraitsButton)) @@ -2672,3 +2673,230 @@ func setViewPersonGeneticAnalysisDiscreteTraitRuleDetailsPage(window fyne.Window } +func setViewPersonGeneticAnalysisNumericTraitsPage(window fyne.Window, personIdentifier string, analysisObject geneticAnalysis.PersonAnalysis, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisNumericTraitsPage(window, personIdentifier, analysisObject, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Numeric Traits") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the numeric traits for this person's genome.") + + getTraitsContainer := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + if (err != nil){ return nil, err } + + // Outputs: + // -[16]byte: Main genome identifier (Is either the combined Only exclude conflicts genome or the only genome) + // -error + getMainGenomeIdentifier := func()([16]byte, error){ + + if (multipleGenomesExist == true){ + return onlyExcludeConflictsGenomeIdentifier, nil + } + // Only 1 genome exists + + genomeIdentifier := allRawGenomeIdentifiersList[0] + + return genomeIdentifier, nil + } + + mainGenomeIdentifier, err := getMainGenomeIdentifier() + if (err != nil){ return nil, err } + + emptyLabel1 := widget.NewLabel("") + traitNameLabel := getItalicLabelCentered("Trait Name") + + emptyLabel2 := widget.NewLabel("") + predictedOutcomeLabel := getItalicLabelCentered("Predicted Outcome") + + quantityOfLabel := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + emptyLabel3 := widget.NewLabel("") + confidenceRangeLabel := getItalicLabelCentered("Confidence Range") + + emptyLabel4 := widget.NewLabel("") + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabel5 := widget.NewLabel("") + emptyLabel6 := widget.NewLabel("") + + traitNameColumn := container.NewVBox(emptyLabel1, traitNameLabel, widget.NewSeparator()) + predictedOutcomeColumn := container.NewVBox(emptyLabel2, predictedOutcomeLabel, widget.NewSeparator()) + confidenceRangeColumn := container.NewVBox(emptyLabel3, confidenceRangeLabel, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel, lociKnownLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(emptyLabel4, conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel5, emptyLabel6, widget.NewSeparator()) + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + continue + } + + traitName := traitObject.TraitName + traitLociList := traitObject.LociList + + if (len(traitLociList) == 0){ + // This trait does not have any loci to analyze + // We cannot analyze it yet + continue + } + + traitNameText := getBoldLabelCentered(traitName) + + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (neuralNetworkExists == false){ + // This trait has no neural network + // We cannot analyze it + continue + } + + analysisExists, predictedOutcome, confidenceRangesMap, quantityOfLociKnown, _, conflictExists, err := readGeneticAnalysis.GetPersonNumericTraitInfoFromGeneticAnalysis(analysisObject, traitName, mainGenomeIdentifier) + if (err != nil) { return nil, err } + + getPredictionLabel := func()fyne.Widget{ + + if (analysisExists == false){ + result := getItalicLabel("Unknown") + return result + } + + predictedOutcomeString := helpers.ConvertFloat64ToStringRounded(predictedOutcome, 2) + + //TODO: Fix units + result := getBoldLabel(predictedOutcomeString + " centimeters") + + return result + } + + predictionLabel := getPredictionLabel() + + predictionLabelCentered := getWidgetCentered(predictionLabel) + + getConfidenceRangeLabel := func()(fyne.Widget, error){ + + if (analysisExists == false){ + unknownLabel := widget.NewLabel("Unknown") + return unknownLabel, nil + } + + // This is a list of the percentage accuracies in the map + // For example: 80% == The distance from the prediction you must travel for 80% of the predictions to be + // accurate within that range + confidenceRangePercentagesList := helpers.GetListOfMapKeys(confidenceRangesMap) + + // We sort the list so the percentage is always the same upon refreshing the page + slices.Sort(confidenceRangePercentagesList) + + closestToEightyPercentage, err := helpers.GetClosestIntInList(confidenceRangePercentagesList, 80) + if (err != nil) { return nil, err } + + closestToEightyPercentageConfidenceDistance, exists := confidenceRangesMap[closestToEightyPercentage] + if (exists == false){ + return nil, errors.New("GetListOfMapKeys returning list of elements which contains element which is not in the map.") + } + + closestConfidenceDistanceString := helpers.ConvertFloat64ToStringRounded(closestToEightyPercentageConfidenceDistance, 2) + + closestToEightyPercentageString := helpers.ConvertIntToString(closestToEightyPercentage) + + confidenceRangeLabelValueFormatted := "+/- " + closestConfidenceDistanceString + " (" + closestToEightyPercentageString + "% Confidence)" + + confidenceRangeLabel := getBoldLabel(confidenceRangeLabelValueFormatted) + + return confidenceRangeLabel, nil + } + + confidenceRangeLabel, err := getConfidenceRangeLabel() + if (err != nil) { return nil, err } + + confidenceRangeLabelCentered := getWidgetCentered(confidenceRangeLabel) + + totalQuantityOfLoci := len(traitLociList) + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + showUnderConstructionDialog(window) + //TODO + })) + + traitNameColumn.Add(traitNameText) + predictedOutcomeColumn.Add(predictionLabelCentered) + confidenceRangeColumn.Add(confidenceRangeLabelCentered) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + conflictExistsColumn.Add(conflictExistsLabel) + viewButtonsColumn.Add(viewDetailsButton) + + traitNameColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + confidenceRangeColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + predictedOutcomeColumn.Add(predictedOutcomeHelpButton) + + confidenceRangeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + confidenceRangeColumn.Add(confidenceRangeHelpButton) + + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) + + traitsContainer := container.NewHBox(layout.NewSpacer(), traitNameColumn, predictedOutcomeColumn, confidenceRangeColumn, quantityOfLociKnownColumn) + + if (multipleGenomesExist == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + + conflictExistsColumn.Add(conflictExistsHelpButton) + traitsContainer.Add(conflictExistsColumn) + } + + traitsContainer.Add(viewButtonsColumn) + traitsContainer.Add(layout.NewSpacer()) + + return traitsContainer, nil + } + + traitsContainer, err := getTraitsContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), traitsContainer) + + setPageContent(page, window) +} + + diff --git a/gui/viewProfileGui.go b/gui/viewProfileGui.go index 9175f07..849591c 100644 --- a/gui/viewProfileGui.go +++ b/gui/viewProfileGui.go @@ -3555,8 +3555,7 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, getAnyUserProfileA }) numericTraitsButton := widget.NewButton("Numeric Traits", func(){ - //TODO - showUnderConstructionDialog(window) + setViewMateProfilePage_NumericGeneticTraits(window, "Offspring", getAnyUserProfileAttributeFunction, currentPage) }) buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, discreteTraitsButton, numericTraitsButton)) @@ -4400,6 +4399,364 @@ func setViewMateProfilePage_DiscreteTraitRules(window fyne.Window, traitName str setPageContent(page, window) } + +func setViewMateProfilePage_NumericGeneticTraits(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + if (userOrOffspring != "User" && userOrOffspring != "Offspring"){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_NumericGeneticTraits called with invalid userOrOffspring: " + userOrOffspring), previousPage) + return + } + + if (userOrOffspring == "Offspring"){ + setLoadingScreen(window, "View Profile - Physical", "Computing Genetic Analysis...") + } + + //currentPage := func(){setViewMateProfilePage_NumericGeneticTraits(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Numeric Genetic Traits")) + + description1 := getLabelCentered("Below is the numeric genetic trait analysis for this user.") + description2 := getLabelCentered("You can choose to view the analysis of the user or an offspring between you and the user.") + description3 := getLabelCentered("You must link your genome person in the Build Profile menu to see offspring information.") + + handleSelectButton := func(newUserOrOffspring string){ + if (userOrOffspring == newUserOrOffspring){ + return + } + setViewMateProfilePage_NumericGeneticTraits(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) + } + + userOrOffspringSelector := widget.NewSelect([]string{"User", "Offspring"}, handleSelectButton) + userOrOffspringSelector.Selected = userOrOffspring + + userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) + + getTraitsInfoGrid := func()(*fyne.Container, error){ + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return nil, err } + + //Outputs: + // -map[int64]locusValue.LocusValue + // -error + getMyGenomeLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ + + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + emptyMap := make(map[int64]locusValue.LocusValue) + return emptyMap, nil + } + + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myAnalysisObject) + if (err != nil) { return nil, err } + + myGenomeLocusValuesMap, exists := myGenomesMap[myGenomeIdentifier] + if (exists == false){ + return nil, errors.New("GetMyChosenMateGeneticAnalysis returning analysis which is missing genome with myGenomeIdentifier.") + } + + return myGenomeLocusValuesMap, nil + } + + myGenomeLocusValuesMap, err := getMyGenomeLocusValuesMap() + if (err != nil) { return nil, err } + + + emptyLabel1 := widget.NewLabel("") + traitNameLabel := getItalicLabelCentered("Trait Name") + + emptyLabel2 := widget.NewLabel("") + userPredictedOutcomeTitle := getItalicLabelCentered("User Predicted Outcome") + + emptyLabel3 := widget.NewLabel("") + offspringPredictedOutcomeTitle := getItalicLabelCentered("Offspring Predicted Outcome") + + predictionTitle := getItalicLabelCentered("Prediction") + confidenceRangeTitle := getItalicLabelCentered("Confidence Range") + + quantityOfLabel1 := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") + + traitNameColumn := container.NewVBox(emptyLabel1, traitNameLabel, widget.NewSeparator()) + userPredictedOutcomeColumn := container.NewVBox(emptyLabel2, userPredictedOutcomeTitle, widget.NewSeparator()) + + offspringPredictedOutcomeColumn := container.NewVBox(emptyLabel3, offspringPredictedOutcomeTitle, widget.NewSeparator()) + + predictionConfidenceRangeColumn := container.NewVBox(predictionTitle, confidenceRangeTitle, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel1, lociKnownLabel, widget.NewSeparator()) + viewTraitDetailsButtonsColumn := container.NewVBox(emptyLabel4, emptyLabel5, widget.NewSeparator()) + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + continue + } + + traitLociList := traitObject.LociList + + numberOfTraitLoci := len(traitLociList) + + if (numberOfTraitLoci == 0){ + // We are not able to analyze these traits + continue + } + + traitNeuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (traitNeuralNetworkExists == false){ + // We are not able to analyze these traits yet + continue + } + + traitNameText := getBoldLabelCentered(translate(traitName)) + + // We construct the user's trait locus values map + // Map Structure: Locus rsID -> locusValue.LocusValue + userTraitLocusValuesMap := make(map[int64]locusValue.LocusValue) + + for _, rsID := range traitLociList{ + + rsIDString := helpers.ConvertInt64ToString(rsID) + + userLocusValueAttributeName := "LocusValue_rs" + rsIDString + + userLocusValueIsKnown, _, userLocusValue, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) + if (err != nil) { return nil, err } + if (userLocusValueIsKnown == false){ + continue + } + + userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusValue, ";") + if (semicolonFound == false){ + return nil, errors.New("Database corrupt: Contains profile with invalid " + userLocusValueAttributeName + " value: " + userLocusValue) + } + + userLocusIsPhasedAttributeName := "LocusIsPhased_rs" + rsIDString + + userLocusIsPhasedExists, _, userLocusIsPhasedString, err := getAnyUserProfileAttributeFunction(userLocusIsPhasedAttributeName) + if (err != nil) { return nil, err } + if (userLocusIsPhasedExists == false){ + return nil, errors.New("Database corrupt: Contains profile with locusValue but not locusIsPhased status for locus: " + rsIDString) + } + + userLocusIsPhased, err := helpers.ConvertYesOrNoStringToBool(userLocusIsPhasedString) + if (err != nil) { return nil, err } + + userLocusValueObject := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + LocusIsPhased: userLocusIsPhased, + } + + userTraitLocusValuesMap[rsID] = userLocusValueObject + } + + //Outputs: + // -bool: Analysis results exist + // -float64: Predicted outcome + // -map[int]float64: Prediction confidence ranges map + // -int: Quantity of known loci + // -error + getAnalysisResults := func()(bool, float64, map[int]float64, int, error){ + + if (userOrOffspring == "User"){ + + traitNeuralNetworkExists, anyLocusValuesAreKnown, predictedOutcome, predictionAccuracyRangesMap, quantityOfLociKnown, _, err := createPersonGeneticAnalysis.GetGenomeNumericTraitAnalysis(traitObject, userTraitLocusValuesMap, true) + if (err != nil) { return false, 0, nil, 0, err } + if (traitNeuralNetworkExists == false){ + return false, 0, nil, 0, errors.New("GetGenomeNumericTraitAnalysis claims neural network doesn't exist for trait, but we already checked.") + } + + return anyLocusValuesAreKnown, predictedOutcome, predictionAccuracyRangesMap, quantityOfLociKnown, nil + } else { + + // userOrOffspring == "Offspring" + + neuralNetworkExists, anyLociKnown, predictedOutcome, predictionAccuracyRangesMap, _, quantityOfLociKnown, _, err := createCoupleGeneticAnalysis.GetOffspringNumericTraitInfo(traitObject, userTraitLocusValuesMap, myGenomeLocusValuesMap) + if (err != nil) { return false, 0, nil, 0, err } + if (neuralNetworkExists == false){ + return false, 0, nil, 0, errors.New("GetOffspringTraitInfo_NeuralNetwork claiming that neural network doesn't exist when we already checked.") + } + + return anyLociKnown, predictedOutcome, predictionAccuracyRangesMap, quantityOfLociKnown, nil + } + } + + analysisExists, predictedOutcome, predictionConfidenceRangesMap, quantityOfLociKnown, err := getAnalysisResults() + if (err != nil) { return nil, err } + + getPredictedOutcomeLabel := func()fyne.Widget{ + + if (analysisExists == false){ + unknownLabel := widget.NewLabel("Unknown") + return unknownLabel + } + + predictedOutcomeString := helpers.ConvertFloat64ToStringRounded(predictedOutcome, 2) + + //TODO: Retrieve units from traits package + predictedOutcomeFormatted := predictedOutcomeString + " centimeters" + + predictedOutcomeLabel := getBoldLabel(predictedOutcomeFormatted) + + return predictedOutcomeLabel + } + + predictedOutcomeLabel := getPredictedOutcomeLabel() + + predictedOutcomeLabelCentered := getWidgetCentered(predictedOutcomeLabel) + + getConfidenceRangeLabel := func()(fyne.Widget, error){ + if (analysisExists == false){ + + result := widget.NewLabel("Unknown") + return result, nil + } + + // This is a list of the percentage accuracies in the map + // For example: 80% == The distance from the prediction you must travel for 80% of the predictions to be + // accurate within that range + confidenceRangePercentagesList := helpers.GetListOfMapKeys(predictionConfidenceRangesMap) + + // We sort the list so the percentage is always the same upon refreshing the page + slices.Sort(confidenceRangePercentagesList) + + closestToEightyPercentage, err := helpers.GetClosestIntInList(confidenceRangePercentagesList, 80) + if (err != nil) { return nil, err } + + closestToEightyPercentageConfidenceDistance, exists := predictionConfidenceRangesMap[closestToEightyPercentage] + if (exists == false){ + return nil, errors.New("GetListOfMapKeys returning list of elements which contains element which is not in the map.") + } + + closestConfidenceDistanceString := helpers.ConvertFloat64ToStringRounded(closestToEightyPercentageConfidenceDistance, 2) + + closestToEightyPercentageString := helpers.ConvertIntToString(closestToEightyPercentage) + + confidenceRangeLabelValueFormatted := "+/- " + closestConfidenceDistanceString + " (" + closestToEightyPercentageString + "% Confidence)" + + confidenceRangeLabel := getBoldLabel(confidenceRangeLabelValueFormatted) + + return confidenceRangeLabel, nil + } + + confidenceRangeLabel, err := getConfidenceRangeLabel() + if (err != nil) { return nil, err } + + confidenceRangeLabelCentered := getWidgetCentered(confidenceRangeLabel) + + totalNumberOfLociString := helpers.ConvertIntToString(numberOfTraitLoci) + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalNumberOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + viewTraitDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + traitNameColumn.Add(traitNameText) + + if (userOrOffspring == "User"){ + userPredictedOutcomeColumn.Add(predictedOutcomeLabelCentered) + } else { + offspringPredictedOutcomeColumn.Add(predictedOutcomeLabelCentered) + } + + predictionConfidenceRangeColumn.Add(confidenceRangeLabelCentered) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + viewTraitDetailsButtonsColumn.Add(viewTraitDetailsButton) + + traitNameColumn.Add(widget.NewSeparator()) + userPredictedOutcomeColumn.Add(widget.NewSeparator()) + offspringPredictedOutcomeColumn.Add(widget.NewSeparator()) + predictionConfidenceRangeColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) + viewTraitDetailsButtonsColumn.Add(widget.NewSeparator()) + } + + if (userOrOffspring == "User"){ + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + //TODO + showUnderConstructionDialog(window) + }) + + userPredictedOutcomeColumn.Add(predictedOutcomeHelpButton) + + } else { + // userOrOffspring == "Offspring" + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + //TODO + showUnderConstructionDialog(window) + }) + + offspringPredictedOutcomeColumn.Add(predictedOutcomeHelpButton) + } + + predictionConfidenceRangeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + //TODO + showUnderConstructionDialog(window) + }) + + predictionConfidenceRangeColumn.Add(predictionConfidenceRangeHelpButton) + + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) + + traitsInfoGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn) + + if (userOrOffspring == "User"){ + + traitsInfoGrid.Add(userPredictedOutcomeColumn) + + } else { + + // userOrOffspring == "Offspring" + + traitsInfoGrid.Add(offspringPredictedOutcomeColumn) + } + + traitsInfoGrid.Add(predictionConfidenceRangeColumn) + traitsInfoGrid.Add(quantityOfLociKnownColumn) + traitsInfoGrid.Add(viewTraitDetailsButtonsColumn) + traitsInfoGrid.Add(layout.NewSpacer()) + + return traitsInfoGrid, nil + } + + traitsInfoGrid, err := getTraitsInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator(), traitsInfoGrid) + + setPageContent(page, window) +} + func setViewMateProfilePage_Diet(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ title := getPageTitleCentered(translate("View Profile - Lifestyle")) diff --git a/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go b/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go index 77aa75d..7dfbd75 100644 --- a/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go +++ b/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go @@ -41,7 +41,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom person1PrepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ newPercentageCompletion, err := helpers.ScaleIntProportionally(true, newPercentage, 0, 100, 0, 25) - if (err != nil){ return err } + if (err != nil) { return err } err = updatePercentageCompleteFunction(newPercentageCompletion) if (err != nil) { return err } @@ -742,6 +742,104 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom } offspringDiscreteTraitsMap[traitName] = newOffspringTraitInfoObject + + } else { + + // traitIsDiscreteOrNumeric == "Numeric" + + // This map stores the trait info for each genome pair + // Map Structure: Genome Pair Identifier -> OffspringGenomePairNumericTraitInfo + offspringTraitInfoMap := make(map[[32]byte]geneticAnalysis.OffspringGenomePairNumericTraitInfo) + + // This will add the offspring trait information for the provided genome pair to the offspringTraitInfoMap + addGenomePairTraitInfoToOffspringMap := func(person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ + + person1LocusValuesMap, exists := person1GenomesMap[person1GenomeIdentifier] + if (exists == false){ + return errors.New("addGenomePairTraitInfoToOffspringMap called with unknown person1GenomeIdentifier.") + } + + person2LocusValuesMap, exists := person2GenomesMap[person2GenomeIdentifier] + if (exists == false){ + return errors.New("addGenomePairTraitInfoToOffspringMap called with unknown person2GenomeIdentifier.") + } + + neuralNetworkExists, neuralNetworkAnalysisExists, averageOutcome, predictionConfidenceRangesMap, sampleOffspringOutcomesList, quantityOfLociTested, quantityOfParentalPhasedLoci, err := GetOffspringNumericTraitInfo(traitObject, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return err } + if (neuralNetworkExists == false){ + // Predictions are not possible for this trait + return nil + } + if (neuralNetworkAnalysisExists == false){ + // No locations for this trait exists in which both user's genomes contain information + return nil + } + + newOffspringGenomePairTraitInfo := geneticAnalysis.OffspringGenomePairNumericTraitInfo{ + OffspringAverageOutcome: averageOutcome, + PredictionConfidenceRangesMap: predictionConfidenceRangesMap, + QuantityOfParentalPhasedLoci: quantityOfParentalPhasedLoci, + QuantityOfLociKnown: quantityOfLociTested, + SampleOffspringOutcomesList: sampleOffspringOutcomesList, + } + + genomePairIdentifier := helpers.JoinTwo16ByteArrays(person1GenomeIdentifier, person2GenomeIdentifier) + + offspringTraitInfoMap[genomePairIdentifier] = newOffspringGenomePairTraitInfo + + return nil + } + + err = addGenomePairTraitInfoToOffspringMap(pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addGenomePairTraitInfoToOffspringMap(pair2Person1GenomeIdentifier, pair2Person2GenomeIdentifier) + if (err != nil) { return false, "", err } + } + + newOffspringTraitInfoObject := geneticAnalysis.OffspringNumericTraitInfo{ + TraitInfoMap: offspringTraitInfoMap, + } + + if (len(offspringTraitInfoMap) >= 2){ + + // We check for conflicts + // Conflicts are only possible if two genome pairs exist with information about the trait + + checkIfConflictExists := func()(bool, error){ + + // We check for conflicts between each genome pair's outcome scores and trait rules maps + + genomePairTraitInfoObject := geneticAnalysis.OffspringGenomePairNumericTraitInfo{} + + firstItemReached := false + + for _, currentGenomePairTraitInfoObject := range offspringTraitInfoMap{ + + if (firstItemReached == false){ + genomePairTraitInfoObject = currentGenomePairTraitInfoObject + firstItemReached = true + continue + } + + areEqual := reflect.DeepEqual(genomePairTraitInfoObject, currentGenomePairTraitInfoObject) + if (areEqual == false){ + return true, nil + } + } + + return false, nil + } + + conflictExists, err := checkIfConflictExists() + if (err != nil) { return false, "", err } + + newOffspringTraitInfoObject.ConflictExists = conflictExists + } + + offspringNumericTraitsMap[traitName] = newOffspringTraitInfoObject } } @@ -1323,6 +1421,110 @@ func GetOffspringDiscreteTraitInfo_Rules(traitObject traits.Trait, person1LocusV } + +//Outputs: +// -bool: A neural network exists for this trait +// -bool: Analysis exists (at least 1 locus exists for this analysis from both people's genomes +// -float64: Average outcome for offspring +// -map[int]float64: Prediction accuracy ranges map +// -Map Structure: Probability prediction is accurate (X) -> Distance from predictoin that must be travelled in both directions to +// create a range in which the true value will fall into, X% of the time +// -[]float64: A list of 100 offspring outcomes +// -int: Quantity of loci known +// -int: Quantity of parental phased loci +// -error +func GetOffspringNumericTraitInfo(traitObject traits.Trait, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, bool, float64, map[int]float64, []float64, int, int, error){ + + traitName := traitObject.TraitName + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + return false, false, 0, nil, nil, 0, 0, errors.New("GetOffspringNumericTraitInfo called with non-numeric trait.") + } + + modelExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (modelExists == false){ + // Prediction is not possible for this trait + return false, false, 0, nil, nil, 0, 0, nil + } + + traitLociList := traitObject.LociList + + // First we count up the quantity of parental phased loci + // We only count the quantity of phased loci for loci which are known for both parents + + quantityOfParentalPhasedLoci := 0 + + for _, rsID := range traitLociList{ + + person1LocusValue, exists := person1LocusValuesMap[rsID] + if (exists == false){ + continue + } + + person2LocusValue, exists := person2LocusValuesMap[rsID] + if (exists == false){ + continue + } + + person1LocusIsPhased := person1LocusValue.LocusIsPhased + if (person1LocusIsPhased == true){ + quantityOfParentalPhasedLoci += 1 + } + + person2LocusIsPhased := person2LocusValue.LocusIsPhased + if (person2LocusIsPhased == true){ + quantityOfParentalPhasedLoci += 1 + } + } + + // Next, we create 100 prospective offspring genomes. + + anyLocusValueExists, prospectiveOffspringGenomesList, err := getProspectiveOffspringGenomesList(traitLociList, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return false, false, 0, nil, nil, 0, 0, err } + if (anyLocusValueExists == false){ + return true, false, 0, nil, nil, 0, 0, nil + } + + // A list of predicted outcomes for each offspring + predictedOutcomesList := make([]float64, 0) + + accuracyRangesMap := make(map[int]float64) + quantityOfLociTested := 0 + + for index, offspringGenomeMap := range prospectiveOffspringGenomesList{ + + neuralNetworkExists, predictionIsKnown, predictedOutcome, predictionAccuracyRangesMap, currentQuantityOfLociTested, _, err := createPersonGeneticAnalysis.GetGenomeNumericTraitAnalysis(traitObject, offspringGenomeMap, false) + if (err != nil){ return false, false, 0, nil, nil, 0, 0, err } + if (neuralNetworkExists == false){ + return false, false, 0, nil, nil, 0, 0, errors.New("GetGenomeNumericTraitAnalysis claiming that neural network doesn't exist when we already checked.") + } + if (predictionIsKnown == false){ + return false, false, 0, nil, nil, 0, 0, errors.New("GetGenomeNumericTraitAnalysis claiming that prediction is impossible when we already know at least 1 locus value exists for trait.") + } + + predictedOutcomesList = append(predictedOutcomesList, predictedOutcome) + + if (index == 0){ + // These values should be the same for each predicted offspring + accuracyRangesMap = predictionAccuracyRangesMap + quantityOfLociTested = currentQuantityOfLociTested + } + } + + // We calculate the average predicted outcome + + outcomesSum := float64(0) + + for _, predictedOutcome := range predictedOutcomesList{ + outcomesSum += predictedOutcome + } + + averageOutcome := outcomesSum/100 + + return true, true, averageOutcome, accuracyRangesMap, predictedOutcomesList, quantityOfLociTested, quantityOfParentalPhasedLoci, nil +} + // This function will return a list of 100 prospective offspring genomes // Each genome represents an equal-probability offspring genome from both people's genomes // This function takes into account the effects of genetic linkage diff --git a/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go b/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go index e56f753..43cf8fd 100644 --- a/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go +++ b/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go @@ -150,6 +150,14 @@ func CreatePersonGeneticAnalysis(genomesList []prepareRawGenomes.RawGenomeWithMe if (err != nil) { return false, "", err } analysisDiscreteTraitsMap[traitName] = personTraitAnalysisObject + } else { + + //traitIsDiscreteOrNumeric == "Numeric" + + personTraitAnalysisObject, err := GetPersonNumericTraitAnalysis(genomesWithMetadataList, traitObject) + if (err != nil) { return false, "", err } + + analysisNumericTraitsMap[traitName] = personTraitAnalysisObject } } @@ -993,6 +1001,79 @@ func GetPersonDiscreteTraitAnalysis(inputGenomesWithMetadataList []prepareRawGen } +//Outputs: +// -geneticAnalysis.PersonNumericTraitInfo: Trait analysis object +// -error +func GetPersonNumericTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.GenomeWithMetadata, traitObject traits.Trait)(geneticAnalysis.PersonNumericTraitInfo, error){ + + // Map Structure: Genome Identifier -> PersonGenomeNumericTraitInfo + newPersonTraitInfoMap := make(map[[16]byte]geneticAnalysis.PersonGenomeNumericTraitInfo) + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + genomeMap := genomeWithMetadataObject.GenomeMap + + neuralNetworkExists, neuralNetworkOutcomeIsKnown, predictedOutcome, predictionConfidenceRangesMap, quantityOfLociKnown, quantityOfPhasedLoci, err := GetGenomeNumericTraitAnalysis(traitObject, genomeMap, true) + if (err != nil) { return geneticAnalysis.PersonNumericTraitInfo{}, err } + if (neuralNetworkExists == false || neuralNetworkOutcomeIsKnown == false){ + continue + } + + newPersonGenomeTraitInfo := geneticAnalysis.PersonGenomeNumericTraitInfo{ + PredictedOutcome: predictedOutcome, + ConfidenceRangesMap: predictionConfidenceRangesMap, + QuantityOfLociKnown: quantityOfLociKnown, + QuantityOfPhasedLoci: quantityOfPhasedLoci, + } + + newPersonTraitInfoMap[genomeIdentifier] = newPersonGenomeTraitInfo + } + + newPersonTraitInfoObject := geneticAnalysis.PersonNumericTraitInfo{ + TraitInfoMap: newPersonTraitInfoMap, + } + + if (len(newPersonTraitInfoMap) <= 1){ + // We do not need to check for conflicts, there is only <=1 genome with trait information + // Nothing left to do. Analysis is complete. + return newPersonTraitInfoObject, nil + } + + // We check for conflicts + + getConflictExistsBool := func()(bool, error){ + + // We check to see if the analysis results are the same for all genomes + + firstItemReached := false + personGenomeTraitInfoObject := geneticAnalysis.PersonGenomeNumericTraitInfo{} + + for _, genomeTraitInfoObject := range newPersonTraitInfoMap{ + + if (firstItemReached == false){ + personGenomeTraitInfoObject = genomeTraitInfoObject + continue + } + + areEqual := reflect.DeepEqual(personGenomeTraitInfoObject, genomeTraitInfoObject) + if (areEqual == false){ + return true, nil + } + } + + return false, nil + } + + conflictExists, err := getConflictExistsBool() + if (err != nil) { return geneticAnalysis.PersonNumericTraitInfo{}, err } + + newPersonTraitInfoObject.ConflictExists = conflictExists + + return newPersonTraitInfoObject, nil +} + + //Outputs: // -int: Base pair disease locus risk weight // -bool: Base pair disease locus odds ratio known @@ -1021,7 +1102,7 @@ func GetGenomePolygenicDiseaseLocusRiskInfo(locusRiskWeightsMap map[string]int, return riskWeight, true, oddsRatio, nil } -// We use this to generate trait predictions using a neural network +// We use this to generate discrete trait predictions using a neural network // The alternative prediction method is to use Rules (see GetGenomeTraitAnalysis_Rules) //Outputs: // -bool: Trait Neural network analysis available (if false, we can't predict this trait using a neural network) @@ -1263,6 +1344,65 @@ func GetGenomePassesDiscreteTraitRuleStatus(ruleLociList []traits.RuleLocus, gen } +// We use this to generate numeric trait predictions using a neural network +//Outputs: +// -bool: Trait Neural network analysis available (if false, we can't predict this trait using a neural network) +// -bool: Neural network outcome is known (at least 1 locus value is known which is needed for the neural network +// -float64: The predicted value (Example: Height in centimeters) +// -map[int]float64: Accuracy ranges map +// -Map Structure: Probability prediction is accurate (X) -> Distance from predictoin that must be travelled in both directions to +// create a range in which the true value will fall into, X% of the time +// -int: Quantity of loci known +// -int: Quantity of phased loci +// -error +func GetGenomeNumericTraitAnalysis(traitObject traits.Trait, genomeMap map[int64]locusValue.LocusValue, checkForAliases bool)(bool, bool, float64, map[int]float64, int, int, error){ + + getGenomeLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ + + if (checkForAliases == false){ + // We don't need to check for rsID aliases. + return genomeMap, nil + } + + traitLociList := traitObject.LociList + + // This map contains the locus values for the genome + // If a locus's entry doesn't exist, its value is unknown + // Map Structure: Locus rsID -> Locus Value + genomeLocusValuesMap := make(map[int64]locusValue.LocusValue) + + for _, locusRSID := range traitLociList{ + + locusBasePairKnown, _, _, _, locusValueObject, err := GetLocusValueFromGenomeMap(checkForAliases, genomeMap, locusRSID) + if (err != nil) { return nil, err } + if (locusBasePairKnown == false){ + continue + } + + genomeLocusValuesMap[locusRSID] = locusValueObject + } + + return genomeLocusValuesMap, nil + } + + genomeLocusValuesMap, err := getGenomeLocusValuesMap() + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + traitName := traitObject.TraitName + + neuralNetworkModelExists, traitPredictionIsPossible, predictedOutcome, predictionAccuracyRangesMap, quantityOfLociKnown, quantityOfPhasedLoci, err := geneticPrediction.GetNeuralNetworkNumericTraitPredictionFromGenomeMap(traitName, genomeLocusValuesMap) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + if (neuralNetworkModelExists == false){ + return false, false, 0, nil, 0, 0, nil + } + if (traitPredictionIsPossible == false){ + return true, false, 0, nil, 0, 0, nil + } + + return true, true, predictedOutcome, predictionAccuracyRangesMap, quantityOfLociKnown, quantityOfPhasedLoci, nil +} + + // This function will retrieve the base pair of the locus from the input genome map // We use this function because each rsID has aliases, so we must sometimes check those aliases to find locus values // diff --git a/internal/genetics/geneticAnalysis/geneticAnalysis.go b/internal/genetics/geneticAnalysis/geneticAnalysis.go index 26f4969..9677c8e 100644 --- a/internal/genetics/geneticAnalysis/geneticAnalysis.go +++ b/internal/genetics/geneticAnalysis/geneticAnalysis.go @@ -469,15 +469,15 @@ type OffspringGenomePairNumericTraitInfo struct{ // predicted value's range to be accurate, X% of the time? // For example: 50% accuracy requires a +/-5 point range, 80% accuracy requires a +-15 point range // Map Structure: Accuracy probability (0-100) -> Amount to add to value in both +/- directions so prediction is that accurate - AverageConfidenceRangesMap map[int]float64 + PredictionConfidenceRangesMap map[int]float64 + + QuantityOfLociKnown int // This describes the quantity of loci from both parents that are phased // For example, if there are 10 loci for this trait, and one parent has 10 phased loci and the other has 5, // this variable will have a value of 15 QuantityOfParentalPhasedLoci int - QuantityOfLociKnown int - // A list of 100 offspring outcomes for 100 prospective offspring from the genome pair // Example: A list of heights for 100 prospective offspring SampleOffspringOutcomesList []float64 diff --git a/internal/genetics/geneticPrediction/geneticPrediction.go b/internal/genetics/geneticPrediction/geneticPrediction.go index 94a4941..720b2d1 100644 --- a/internal/genetics/geneticPrediction/geneticPrediction.go +++ b/internal/genetics/geneticPrediction/geneticPrediction.go @@ -4,7 +4,7 @@ package geneticPrediction -// I am a neophyte in the ways of neural networks. +// I am a novice in the ways of neural networks. // Machine learning experts should chime in and offer improvements. // We have to make sure that model inference remains very fast // Sorting matches by offspring total polygenic disease score will require inference on dozens of models for each match @@ -27,7 +27,6 @@ import "encoding/gob" import "slices" import "errors" -//import "log" type NeuralNetwork struct{ @@ -283,13 +282,9 @@ func DecodeBytesToDiscreteTraitPredictionAccuracyInfoMap(inputBytes []byte)(Disc return newDiscreteTraitPredictionAccuracyInfoMap, nil } -type NumericTraitPredictionAccuracyInfoMap map[NumericTraitOutcomeInfo]NumericTraitPredictionAccuracyRangesMap +type NumericTraitPredictionAccuracyInfoMap map[NumericTraitPredictionInfo]NumericTraitPredictionAccuracyRangesMap -type NumericTraitOutcomeInfo struct{ - - // This is the outcome which was predicted - // Example: 150 centimeters - OutcomeValue float64 +type NumericTraitPredictionInfo struct{ // This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction PercentageOfLociTested int @@ -377,54 +372,8 @@ func GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap(traitName string, geno traitRSIDsListCopy := slices.Clone(traitRSIDsList) slices.Sort(traitRSIDsListCopy) - // In the inputLayer, each locus value is represented by 3 neurons: - // 1. LocusExists/LocusIsPhased - // -0 = Locus value is unknown - // -0.5 = Locus Is known, phase is unknown - // -1 = Locus Is Known, phase is known - // 2. Allele1 Locus Value (Value between 0-1) - // -0 = Value is unknown - // 3. Allele2 Locus Value (Value between 0-1) - // -0 = Value is unknown - // - neuralNetworkInput := make([]float32, 0) - - quantityOfLociKnown := 0 - quantityOfPhasedLoci := 0 - - for _, rsID := range traitRSIDsListCopy{ - - userLocusValue, exists := genomeMap[rsID] - if (exists == false){ - neuralNetworkInput = append(neuralNetworkInput, 0, 0, 0) - continue - } - - quantityOfLociKnown += 1 - - locusAllele1 := userLocusValue.Base1Value - locusAllele2 := userLocusValue.Base2Value - locusIsPhased := userLocusValue.LocusIsPhased - - getNeuron1 := func()float32{ - if (locusIsPhased == false){ - return 0.5 - } - - quantityOfPhasedLoci += 1 - return 1 - } - - neuron1 := getNeuron1() - - neuron2, err := convertAlleleToNeuron(locusAllele1) - if (err != nil) { return false, false, "", 0, 0, 0, err } - - neuron3, err := convertAlleleToNeuron(locusAllele2) - if (err != nil) { return false, false, "", 0, 0, 0, err } - - neuralNetworkInput = append(neuralNetworkInput, neuron1, neuron2, neuron3) - } + neuralNetworkInput, quantityOfLociKnown, quantityOfPhasedLoci, err := createInputNeuralNetworkLayerFromGenomeMap(traitRSIDsListCopy, genomeMap) + if (err != nil) { return false, false, "", 0, 0, 0, err } if (quantityOfLociKnown == 0){ // We can't predict anything about this trait for this genome @@ -522,6 +471,179 @@ func GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap(traitName string, geno return true, true, predictedOutcomeName, predictionAccuracy, quantityOfLociKnown, quantityOfPhasedLoci, nil } +//Outputs: +// -bool: Neural network model exists for this trait (trait prediction is possible for this trait) +// -bool: Trait prediction is possible for this user (User has at least 1 known trait locus value) +// -float64: Predicted trait outcome (Example: Height in centimeters) +// -map[int]float64: Accuracy ranges map +// -Map Structure: Probability prediction is accurate (X) -> Distance from prediction that must be travelled in both directions to +// create a range in which the true value will fall into, X% of the time +// -int: Quantity of loci known +// -int: Quantity of phased loci +// -error +func GetNeuralNetworkNumericTraitPredictionFromGenomeMap(traitName string, genomeMap map[int64]locusValue.LocusValue)(bool, bool, float64, map[int]float64, int, int, error){ + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + return false, false, 0, nil, 0, 0, errors.New("GetNeuralNetworkNumericTraitPredictionFromGenomeMap called with non-discrete trait: " + traitName) + } + + // This is a map of rsIDs which influence this trait + traitRSIDsList := traitObject.LociList + + if (len(traitRSIDsList) == 0){ + // Prediction is not possible for this trait + return false, false, 0, nil, 0, 0, nil + } + + predictionModelExists, predictionModelBytes := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (predictionModelExists == false){ + // Prediction is not possible for this trait + return false, false, 0, nil, 0, 0, nil + } + + traitRSIDsListCopy := slices.Clone(traitRSIDsList) + slices.Sort(traitRSIDsListCopy) + + neuralNetworkInput, quantityOfLociKnown, quantityOfPhasedLoci, err := createInputNeuralNetworkLayerFromGenomeMap(traitRSIDsListCopy, genomeMap) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + if (quantityOfLociKnown == 0){ + // We can't predict anything about this trait for this genome + return true, false, 0, nil, 0, 0, nil + } + + neuralNetworkObject, err := DecodeBytesToNeuralNetworkObject(predictionModelBytes) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + outputLayer, err := GetNeuralNetworkRawPrediction(&neuralNetworkObject, true, neuralNetworkInput) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + predictedOutcomeValue, err := GetNumericOutcomeValueFromOutputLayer(traitName, outputLayer) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + modelTraitAccuracyInfoFile, err := geneticPredictionModels.GetPredictionModelNumericTraitAccuracyInfoBytes(traitName) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + modelTraitAccuracyInfoMap, err := DecodeBytesToNumericTraitPredictionAccuracyInfoMap(modelTraitAccuracyInfoFile) + if (err != nil) { return false, false, 0, nil, 0, 0, err } + + // We create a prediction confidence ranges map for our prediction + + getPredictionConfidenceRangesMap := func()map[int]float64{ + + totalNumberOfTraitLoci := len(traitRSIDsList) + + proportionOfLociTested := float64(quantityOfLociKnown)/float64(totalNumberOfTraitLoci) + percentageOfLociTested := int(proportionOfLociTested * 100) + + proportionOfPhasedLoci := float64(quantityOfPhasedLoci)/float64(totalNumberOfTraitLoci) + percentageOfPhasedLoci := int(proportionOfPhasedLoci * 100) + + // This is a value between 0 and 100 that represents the most similar confidence ranges map for this prediction + var closestPredictionConfidenceRangesMap map[int]float64 + + // This is a value that represents the distance our closest prediction confidence ranges map has from the current prediction + // Consider each prediction accuracy value on an (X,Y) coordinate plane + // X = Number of loci tested + // Y = Number of phased loci + closestPredictionConfidenceRangesMapDistance := float64(0) + + for traitOutcomeInfo, traitPredictionConfidenceRangesMap := range modelTraitAccuracyInfoMap{ + + currentPercentageOfLociTested := traitOutcomeInfo.PercentageOfLociTested + currentPercentageOfPhasedLoci := traitOutcomeInfo.PercentageOfPhasedLoci + + // Distance Formula for 2 coordinates (x1, y1) and (x2, y2): + // distance = √((x2 - x1)^2 + (y2 - y1)^2) + + differenceInX := float64(currentPercentageOfLociTested - percentageOfLociTested) + differenceInY := float64(currentPercentageOfPhasedLoci - percentageOfPhasedLoci) + + distance := math.Sqrt(math.Pow(differenceInX, 2) + math.Pow(differenceInY, 2)) + + if (distance == 0){ + // We found the exact prediction confidence ranges map + return traitPredictionConfidenceRangesMap + } + + if (closestPredictionConfidenceRangesMap == nil || distance < closestPredictionConfidenceRangesMapDistance){ + closestPredictionConfidenceRangesMapDistance = distance + closestPredictionConfidenceRangesMap = traitPredictionConfidenceRangesMap + } + } + + return closestPredictionConfidenceRangesMap + } + + predictionConfidenceRangesMap := getPredictionConfidenceRangesMap() + + return true, true, predictedOutcomeValue, predictionConfidenceRangesMap, quantityOfLociKnown, quantityOfPhasedLoci, nil +} + + +//Outputs: +// -[]float32: Input layer for neural network +// -int: Quantity of known loci +// -int: Quantity of phased loci +// -error +func createInputNeuralNetworkLayerFromGenomeMap(rsidsList []int64, genomeMap map[int64]locusValue.LocusValue)([]float32, int, int, error){ + + // In the inputLayer, each locus value is represented by 3 neurons: + // 1. LocusExists/LocusIsPhased + // -0 = Locus value is unknown + // -0.5 = Locus Is known, phase is unknown + // -1 = Locus Is Known, phase is known + // 2. Allele1 Locus Value (Value between 0-1) + // -0 = Value is unknown + // 3. Allele2 Locus Value (Value between 0-1) + // -0 = Value is unknown + // + neuralNetworkInput := make([]float32, 0) + + quantityOfLociKnown := 0 + quantityOfPhasedLoci := 0 + + for _, rsID := range rsidsList{ + + userLocusValue, exists := genomeMap[rsID] + if (exists == false){ + neuralNetworkInput = append(neuralNetworkInput, 0, 0, 0) + continue + } + + quantityOfLociKnown += 1 + + locusAllele1 := userLocusValue.Base1Value + locusAllele2 := userLocusValue.Base2Value + locusIsPhased := userLocusValue.LocusIsPhased + + getNeuron1 := func()float32{ + if (locusIsPhased == false){ + return 0.5 + } + + quantityOfPhasedLoci += 1 + return 1 + } + + neuron1 := getNeuron1() + + neuron2, err := convertAlleleToNeuron(locusAllele1) + if (err != nil) { return nil, 0, 0, err } + + neuron3, err := convertAlleleToNeuron(locusAllele2) + if (err != nil) { return nil, 0, 0, err } + + neuralNetworkInput = append(neuralNetworkInput, neuron1, neuron2, neuron3) + } + + return neuralNetworkInput, quantityOfLociKnown, quantityOfLociKnown, nil +} + //Outputs: // -int: Number of loci values that are known // -int: Number of loci values that are known and phased @@ -732,7 +854,7 @@ func getNeuralNetworkLayerSizes(traitName string)(int, int, int, int, error){ case "Height":{ // There are 3000 input neurons // There is 1 output neuron, representing a height value - return 3000, 2, 2, 1, nil + return 3000, 3, 2, 1, nil } } @@ -1289,21 +1411,16 @@ func TrainNeuralNetwork(traitName string, traitIsNumeric bool, neuralNetworkObje err = gorgonia.Let(trainingDataExpectedOutputNode, outputTensor) if (err != nil) { return false, err } -// for i:=0; i < 10; i++{ + err = virtualMachine.RunAll() + if (err != nil) { return false, err } - err = virtualMachine.RunAll() - if (err != nil) { return false, err } + // NodesToValueGrads is a utility function that converts a Nodes to a slice of ValueGrad for the solver + valueGrads := gorgonia.NodesToValueGrads(neuralNetworkLearnables) - // NodesToValueGrads is a utility function that converts a Nodes to a slice of ValueGrad for the solver - valueGrads := gorgonia.NodesToValueGrads(neuralNetworkLearnables) + err = solver.Step(valueGrads) + if (err != nil) { return false, err } - err = solver.Step(valueGrads) - if (err != nil) { return false, err } - - virtualMachine.Reset() -// } - -// log.Println(cost.Value()) + virtualMachine.Reset() } return true, nil diff --git a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go index a2ddffa..1039c3a 100644 --- a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go +++ b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go @@ -892,6 +892,78 @@ func GetOffspringDiscreteTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject g } + +//Outputs: +// -bool: Any analysis exists +// -float64: Predicted outcome (Example: Height in centimeters) +// -map[int]float64: Prediction confidence ranges map +// -Map Structure: Percentage probability of accurate prediction -> distance of range in both directions from prediction +// -int: Quantity of loci known +// -int: Quantity of phased loci +// -bool: Conflict exists (between any of these results for each genome) +// -error +func GetPersonNumericTraitInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, genomeIdentifier [16]byte)(bool, float64, map[int]float64, int, int, bool, error){ + + personTraitsMap := personAnalysisObject.NumericTraitsMap + + personTraitInfoObject, exists := personTraitsMap[traitName] + if (exists == false){ + return false, 0, nil, 0, 0, false, nil + } + + personTraitInfoMap := personTraitInfoObject.TraitInfoMap + conflictExists := personTraitInfoObject.ConflictExists + + personGenomeTraitInfoObject, exists := personTraitInfoMap[genomeIdentifier] + if (exists == false){ + return false, 0, nil, 0, 0, false, nil + } + + predictedOutcome := personGenomeTraitInfoObject.PredictedOutcome + confidenceRangesMap := personGenomeTraitInfoObject.ConfidenceRangesMap + quantityOfLociKnown := personGenomeTraitInfoObject.QuantityOfLociKnown + quantityOfPhasedLoci := personGenomeTraitInfoObject.QuantityOfPhasedLoci + + return true, predictedOutcome, confidenceRangesMap, quantityOfLociKnown, quantityOfPhasedLoci, conflictExists, nil +} + + + +//Outputs: +// -bool: Analysis exists +// -float64: Average offspring outcome +// -map[int]float64: Prediction confidence ranges map +// -int: Quantity of loci known +// -int: Quantity of Parental phased loci +// -[]float64: 100 Sample offspring outcomes +// -bool: Conflict exists (Between this genome pair and other genome pairs) +// -error +func GetOffspringNumericTraitInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte)(bool, float64, map[int]float64, int, int, []float64, bool, error){ + + offspringTraitsMap := coupleAnalysisObject.NumericTraitsMap + + traitInfoObject, exists := offspringTraitsMap[traitName] + if (exists == false){ + return false, 0, nil, 0, 0, nil, false, nil + } + + traitInfoMap := traitInfoObject.TraitInfoMap + conflictExists := traitInfoObject.ConflictExists + + genomePairTraitInfoObject, exists := traitInfoMap[genomePairIdentifier] + if (exists == false){ + return false, 0, nil, 0, 0, nil, false, nil + } + + offspringAverageOutcome := genomePairTraitInfoObject.OffspringAverageOutcome + predictionConfidenceRangesMap := genomePairTraitInfoObject.PredictionConfidenceRangesMap + quantityOfLociKnown := genomePairTraitInfoObject.QuantityOfLociKnown + quantityOfParentalPhasedLoci := genomePairTraitInfoObject.QuantityOfParentalPhasedLoci + sampleOffspringOutcomesList := genomePairTraitInfoObject.SampleOffspringOutcomesList + + return true, offspringAverageOutcome, predictionConfidenceRangesMap, quantityOfLociKnown, quantityOfParentalPhasedLoci, sampleOffspringOutcomesList, conflictExists, nil +} + // We use this function to verify a person genetic analysis is well formed //TODO: Perform sanity checks on data func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis)error{ @@ -995,6 +1067,13 @@ func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnal if (err != nil) { return err } } } + } else { + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, _, _, _, _, err := GetPersonNumericTraitInfoFromGeneticAnalysis(personAnalysisObject, traitName, genomeIdentifier) + if (err != nil) { return err } + } } } @@ -1111,6 +1190,14 @@ func VerifyCoupleGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnal if (err != nil) { return err } } } + } else { + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, _, _, _, _, err := GetOffspringNumericTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) + if (err != nil) { return err } + } + } } diff --git a/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack index 558c216..f85b810 100644 Binary files a/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack and b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack differ diff --git a/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack b/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack index dfe7e28..8d0f514 100644 Binary files a/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack and b/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack differ diff --git a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack index dc32f26..dff7be3 100644 Binary files a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack and b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack differ diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 29bde7c..749065f 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -2092,3 +2092,49 @@ func Split32ByteArrayInHalf(inputArray [32]byte)([16]byte, [16]byte){ return piece1, piece2 } + + +// This function takes a list of ints and a target value, and returns the int in the list that is the closest to that value +// If there is a tie, the function returns the earliest item in the list of the tied elements +func GetClosestIntInList(inputList []int, targetValue int)(int, error){ + + if (len(inputList) == 0){ + return 0, errors.New("GetClosestIntInList called with empty inputList.") + } + + closestValue := 0 + closestValueDistance := float64(0) + + for index, element := range inputList{ + + if (element == targetValue){ + return element, nil + } + + distance := math.Abs(float64(element - targetValue)) + + if (index == 0 || distance < closestValueDistance){ + closestValue = element + closestValueDistance = distance + } + } + + return closestValue, nil +} + + +func CountMatchingElementsInSlice[E comparable](inputSlice []E, inputElement E)int{ + + counter := 0 + + for _, element := range inputSlice{ + + if (element == inputElement){ + counter += 1 + } + } + + return counter +} + + diff --git a/resources/geneticPredictionModels/geneticPredictionModels.go b/resources/geneticPredictionModels/geneticPredictionModels.go index 7b01f4c..3562906 100644 --- a/resources/geneticPredictionModels/geneticPredictionModels.go +++ b/resources/geneticPredictionModels/geneticPredictionModels.go @@ -11,6 +11,17 @@ import _ "embed" import "errors" + +//go:embed predictionModels/EyeColorModel.gob +var predictionModel_EyeColor []byte + +//go:embed predictionModels/LactoseToleranceModel.gob +var predictionModel_LactoseTolerance []byte + +//go:embed predictionModels/HeightModel.gob +var predictionModel_Height []byte + + //Outputs: // -bool: Model exists // -[]byte @@ -24,16 +35,20 @@ func GetGeneticPredictionModelBytes(traitName string)(bool, []byte){ case "Lactose Tolerance":{ return true, predictionModel_LactoseTolerance } + case "Height":{ + return true, predictionModel_Height + } } return false, nil } -//go:embed predictionModels/EyeColorModel.gob -var predictionModel_EyeColor []byte +//go:embed predictionModelAccuracies/EyeColorModelAccuracy.gob +var predictionAccuracy_EyeColor []byte + +//go:embed predictionModelAccuracies/LactoseToleranceModelAccuracy.gob +var predictionAccuracy_LactoseTolerance []byte -//go:embed predictionModels/LactoseToleranceModel.gob -var predictionModel_LactoseTolerance []byte // The files returned by this function are .gob encoded geneticPrediction.DiscreteTraitPredictionAccuracyInfoMap objects func GetPredictionModelDiscreteTraitAccuracyInfoBytes(traitName string)([]byte, error){ @@ -47,12 +62,22 @@ func GetPredictionModelDiscreteTraitAccuracyInfoBytes(traitName string)([]byte, } } - return nil, errors.New("GetPredictionModelTraitAccuracyInfoFile called with unknown traitName: " + traitName) + return nil, errors.New("GetPredictionModelDiscreteTraitAccuracyInfoBytes called with unknown traitName: " + traitName) } -//go:embed predictionModelAccuracies/EyeColorModelAccuracy.gob -var predictionAccuracy_EyeColor []byte +//go:embed predictionModelAccuracies/HeightModelAccuracy.gob +var predictionAccuracy_Height []byte + +// The files returned by this function are .gob encoded geneticPrediction.NumericTraitPredictionAccuracyInfoMap objects +func GetPredictionModelNumericTraitAccuracyInfoBytes(traitName string)([]byte, error){ + + switch traitName{ + case "Height":{ + return predictionAccuracy_Height, nil + } + } + + return nil, errors.New("GetPredictionModelNumericTraitAccuracyInfoBytes called with unknown traitName: " + traitName) +} -//go:embed predictionModelAccuracies/LactoseToleranceModelAccuracy.gob -var predictionAccuracy_LactoseTolerance []byte diff --git a/resources/geneticPredictionModels/geneticPredictionModels_test.go b/resources/geneticPredictionModels/geneticPredictionModels_test.go index 8468235..da206d7 100644 --- a/resources/geneticPredictionModels/geneticPredictionModels_test.go +++ b/resources/geneticPredictionModels/geneticPredictionModels_test.go @@ -9,7 +9,7 @@ import "seekia/internal/genetics/geneticPrediction" func TestGeneticPredictionModels(t *testing.T){ - traitNamesList := []string{"Eye Color", "Lactose Tolerance"} + traitNamesList := []string{"Eye Color", "Lactose Tolerance", "Height"} for _, traitName := range traitNamesList{ @@ -42,6 +42,21 @@ func TestGeneticPredictionModelAccuracies(t *testing.T){ t.Fatalf("DecodeBytesToDiscreteTraitPredictionAccuracyInfoMap failed: " + err.Error()) } } + + numericTraitNamesList := []string{"Height"} + + for _, traitName := range numericTraitNamesList{ + + accuracyInfoBytes, err := geneticPredictionModels.GetPredictionModelNumericTraitAccuracyInfoBytes(traitName) + if (err != nil){ + t.Fatalf("GetPredictionModelNumericTraitAccuracyInfoBytes failed: " + err.Error()) + } + + _, err = geneticPrediction.DecodeBytesToNumericTraitPredictionAccuracyInfoMap(accuracyInfoBytes) + if (err != nil){ + t.Fatalf("DecodeBytesToNumericTraitPredictionAccuracyInfoMap failed: " + err.Error()) + } + } } diff --git a/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob b/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob index dd8cb7b..4464c40 100644 Binary files a/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob and b/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob differ diff --git a/resources/geneticPredictionModels/predictionModelAccuracies/HeightModelAccuracy.gob b/resources/geneticPredictionModels/predictionModelAccuracies/HeightModelAccuracy.gob new file mode 100644 index 0000000..b2e886a Binary files /dev/null and b/resources/geneticPredictionModels/predictionModelAccuracies/HeightModelAccuracy.gob differ diff --git a/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob b/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob index 6672f75..1ec8a15 100644 Binary files a/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob and b/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob differ diff --git a/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob b/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob index 414c2d1..42434f5 100644 Binary files a/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob and b/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob differ diff --git a/resources/geneticPredictionModels/predictionModels/HeightModel.gob b/resources/geneticPredictionModels/predictionModels/HeightModel.gob new file mode 100644 index 0000000..43104cb Binary files /dev/null and b/resources/geneticPredictionModels/predictionModels/HeightModel.gob differ diff --git a/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob b/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob index a34a8a0..5e8b489 100644 Binary files a/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob and b/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob differ diff --git a/utilities/createGeneticModels/createGeneticModels.go b/utilities/createGeneticModels/createGeneticModels.go index 202fc80..09cb691 100644 --- a/utilities/createGeneticModels/createGeneticModels.go +++ b/utilities/createGeneticModels/createGeneticModels.go @@ -1168,12 +1168,24 @@ func setStartAndMonitorTrainModelPage(window fyne.Window, traitName string, prev neuralNetworkObject, err := geneticPrediction.GetNewUntrainedNeuralNetworkObject(traitName) if (err != nil) { return false, err } - numberOfTrainingDatas := len(trainingSetFilepathsList) - numberOfTrainingDatasString := helpers.ConvertIntToString(numberOfTrainingDatas) + // The number of rounds of training for the training data set + totalQuantityOfRoundsToRun := 2 + + 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 @@ -1190,9 +1202,24 @@ func setStartAndMonitorTrainModelPage(window fyne.Window, traitName string, prev return true, false, geneticPrediction.TrainingData{}, nil } - if (trainingDataIndex == numberOfTrainingDatas){ - // We are done training. - return false, 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] @@ -1206,15 +1233,15 @@ func setStartAndMonitorTrainModelPage(window fyne.Window, traitName string, prev trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents) if (err != nil) { return false, false, geneticPrediction.TrainingData{}, err } - trainingDataIndex += 1 + quantityOfExamplesTrained += 1 - numberOfExamplesTrainedString := helpers.ConvertIntToString(trainingDataIndex + 1) - numberOfExamplesProgress := "Trained " + numberOfExamplesTrainedString + "/" + numberOfTrainingDatasString + " Examples" + quantityOfExamplesTrainedString := helpers.ConvertIntToString(quantityOfExamplesTrained) + numberOfExamplesProgress := "Trained " + quantityOfExamplesTrainedString + "/" + quantityOfTrainingDatasString + " Examples" progressDetailsBinding.Set(numberOfExamplesProgress) - newProgressFloat64 := float64(trainingDataIndex)/float64(numberOfTrainingDatas) + newProgressFloat64 := float64(quantityOfExamplesTrained)/float64(quantityOfTrainingDatas) err = progressPercentageBinding.Set(newProgressFloat64) if (err != nil) { return false, false, geneticPrediction.TrainingData{}, err } @@ -1239,7 +1266,7 @@ func setStartAndMonitorTrainModelPage(window fyne.Window, traitName string, prev } traitIsNumeric := getTraitIsNumericBool() - + processCompleted, err := geneticPrediction.TrainNeuralNetwork(traitName, traitIsNumeric, neuralNetworkObject, getNextTrainingDataFunction) if (err != nil) { return false, err } if (processCompleted == false){ @@ -1661,9 +1688,8 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ // We use this map to count up the information about predictions // We use information from this map to construct the final accuracy information map - // Map Structure: NumericTraitOutcomeInfo -> List of true outcomes - traitPredictionInfoMap := make(map[geneticPrediction.NumericTraitOutcomeInfo][]float64) - + // Map Structure: NumericTraitPredictionInfo -> []float64 (List of distances for each prediction) + traitPredictionInfoMap := make(map[geneticPrediction.NumericTraitPredictionInfo][]float64) _, testingSetFilepathsList, err := getTrainingAndTestingDataFilepathLists(traitName) if (err != nil) { return false, nil, err } @@ -1712,17 +1738,17 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ trainingDataInputLayer := trainingDataObject.InputLayer trainingDataExpectedOutputLayer := trainingDataObject.OutputLayer - predictionLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(&neuralNetworkObject, false, trainingDataInputLayer) + if (len(trainingDataExpectedOutputLayer) != 1){ + return false, nil, 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, nil, err } if (len(predictionLayer) != 1){ return false, nil, errors.New("Neural network numeric prediction output layer length is not 1.") } - if (len(trainingDataExpectedOutputLayer) != 1){ - return false, nil, errors.New("Neural network training data prediction output layer length is not 1.") - } - correctOutcomeValue, err := geneticPrediction.GetNumericOutcomeValueFromOutputLayer(traitName, trainingDataExpectedOutputLayer) if (err != nil) { return false, nil, err } @@ -1738,18 +1764,19 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ proportionOfPhasedLoci := float64(numberOfKnownAndPhasedLoci)/float64(numberOfKnownLoci) percentageOfPhasedLoci := int(100*proportionOfPhasedLoci) - newNumericTraitOutcomeInfo := geneticPrediction.NumericTraitOutcomeInfo{ - OutcomeValue: predictedOutcomeValue, + newNumericTraitPredictionInfo := geneticPrediction.NumericTraitPredictionInfo{ PercentageOfLociTested: percentageOfLociTested, PercentageOfPhasedLoci: percentageOfPhasedLoci, } - existingList, exists := traitPredictionInfoMap[newNumericTraitOutcomeInfo] + distanceFromCorrectValue := math.Abs(predictedOutcomeValue - correctOutcomeValue) + + existingList, exists := traitPredictionInfoMap[newNumericTraitPredictionInfo] if (exists == false){ - traitPredictionInfoMap[newNumericTraitOutcomeInfo] = []float64{correctOutcomeValue} + traitPredictionInfoMap[newNumericTraitPredictionInfo] = []float64{distanceFromCorrectValue} } else { - existingList = append(existingList, correctOutcomeValue) - traitPredictionInfoMap[newNumericTraitOutcomeInfo] = existingList + existingList = append(existingList, distanceFromCorrectValue) + traitPredictionInfoMap[newNumericTraitPredictionInfo] = existingList } exampleIndexString := helpers.ConvertIntToString(index+1) @@ -1764,72 +1791,51 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ // Now we construct the TraitAccuracyInfoMap - // This map stores the accuracy for each outcome - traitPredictionAccuracyInfoMap := make(map[geneticPrediction.NumericTraitOutcomeInfo]geneticPrediction.NumericTraitPredictionAccuracyRangesMap) + // This map stores the accuracy for each QuantityOfKnownLoci/QuantityOfPhasedLoci + traitPredictionAccuracyInfoMap := make(map[geneticPrediction.NumericTraitPredictionInfo]geneticPrediction.NumericTraitPredictionAccuracyRangesMap) - for traitPredictionInfo, realOutcomesList := range traitPredictionInfoMap{ + for traitPredictionInfo, predictionDistancesList := range traitPredictionInfoMap{ - if (len(realOutcomesList) == 0){ - return false, nil, errors.New("traitPredictionInfoMap contains empty realOutcomesList.") + if (len(predictionDistancesList) == 0){ + return false, nil, errors.New("traitPredictionInfoMap contains empty predictionDistancesList.") } - // This is the predicted height value for this set of real outcomes - predictionValue := traitPredictionInfo.OutcomeValue - // Map Structure: Accuracy Percentage (AP) -> Amount needed to deviate from prediction // for the value to be accurate (AP)% of the time newNumericTraitPredictionAccuracyRangesMap := make(map[int]float64) - rangeDistance := float64(0) + if (len(predictionDistancesList) < 5){ + // We don't have enough data to create an accuracyRanges map. + continue + } - for { + // We sort the prediction distances list in ascending order + slices.Sort(predictionDistancesList) - rangeMin := predictionValue - rangeDistance - rangeMax := predictionValue + rangeDistance + finalIndex := len(predictionDistancesList) - 1 - valuesInRangeList := make([]float64, 0) - valuesOutOfRangeList := make([]float64, 0) + for index, distance := range predictionDistancesList{ - for _, outcomeValue := range realOutcomesList{ + proportionOfPredictionsWithinDistance := float64(index)/float64(finalIndex) - if (outcomeValue <= rangeMax && outcomeValue >= rangeMin){ - valuesInRangeList = append(valuesInRangeList, outcomeValue) - } else { - valuesOutOfRangeList = append(valuesOutOfRangeList, outcomeValue) - } + percentageOfPredictionsWithinDistance := int(100 * proportionOfPredictionsWithinDistance) + + if (percentageOfPredictionsWithinDistance == 0){ + // 0% accuracy is not a useful metric for users + continue } - quantityOfValuesInRange := len(valuesInRangeList) - totalQuantityOfValues := len(realOutcomesList) - - proportionOfValuesInRange := float64(quantityOfValuesInRange)/float64(totalQuantityOfValues) - percentageOfValuesInRange := proportionOfValuesInRange * 100 - - if (percentageOfValuesInRange >= 1){ - percentageOfValuesInRangeInt := int(percentageOfValuesInRange) - - newNumericTraitPredictionAccuracyRangesMap[percentageOfValuesInRangeInt] = rangeDistance - } - if (quantityOfValuesInRange == totalQuantityOfValues){ - newNumericTraitPredictionAccuracyRangesMap[100] = rangeDistance - break + _, exists := newNumericTraitPredictionAccuracyRangesMap[percentageOfPredictionsWithinDistance] + if (exists == true){ + // There exists a value for this percentage already + // This happens because we convert a float64 to an int + // The existing percentage must be smaller than our current percentage + // We want to keep that smaller percentage + // For example, we would rather keep the 15.1% value than the 15.8% value. + continue } - // Now we increase rangeDistance - // We find the distance to the next closest item in the list that isn't already in our range - - nearestValueDistance := float64(0) - - for index, outcomeValue := range valuesOutOfRangeList{ - - distance := math.Abs(predictionValue - outcomeValue) - - if (index == 0 || distance < nearestValueDistance){ - nearestValueDistance = distance - } - } - - rangeDistance += nearestValueDistance + newNumericTraitPredictionAccuracyRangesMap[percentageOfPredictionsWithinDistance] = distance } traitPredictionAccuracyInfoMap[traitPredictionInfo] = newNumericTraitPredictionAccuracyRangesMap @@ -1891,8 +1897,8 @@ func setViewModelTestingDiscreteTraitResultsPage(window fyne.Window, traitName s getResultsGrid := func()(*fyne.Container, error){ - outcomeNameTitle := getItalicLabelCentered("Outcome Name") emptyLabel1 := widget.NewLabel("") + outcomeNameTitle := getItalicLabelCentered("Outcome Name") predictionAccuracyTitle1 := getItalicLabelCentered("Prediction Accuracy") knownLociLabel_0to33 := getItalicLabelCentered("0-33% Known Loci") @@ -1903,7 +1909,7 @@ func setViewModelTestingDiscreteTraitResultsPage(window fyne.Window, traitName s predictionAccuracyTitle3 := getItalicLabelCentered("Prediction Accuracy") knownLociLabel_67to100 := getItalicLabelCentered("67-100% Known Loci") - outcomeNameColumn := container.NewVBox(outcomeNameTitle, emptyLabel1, widget.NewSeparator()) + 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())