diff --git a/Changelog.md b/Changelog.md index 3647f5c..5e73d7a 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 neural network trait prediction to genetic analyses. - *Simon Sarasova* * Improved the Create Genetic Models utility and neural network training code. Models are now able to predict traits with some accuracy. - *Simon Sarasova* * Improved ReadMe.md. - *Simon Sarasova* * Improved Seekia's slogan and Whitepaper.md. - *Simon Sarasova* diff --git a/Contributors.md b/Contributors.md index 1c15447..8b2ea99 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 | 265 \ No newline at end of file +Simon Sarasova | June 13, 2023 | 266 \ No newline at end of file diff --git a/gui/buildProfileGui_Lifestyle.go b/gui/buildProfileGui_Lifestyle.go index 3d8bae0..d272e9a 100644 --- a/gui/buildProfileGui_Lifestyle.go +++ b/gui/buildProfileGui_Lifestyle.go @@ -424,7 +424,7 @@ func setBuildMateProfilePage_Wealth(window fyne.Window, previousPage func()){ } isLowerBoundHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setWealthOrIncomeIsLowerBoundExplainerPage(window, currentPage) + setWealthIsLowerBoundExplainerPage(window, currentPage) }) isLowerBoundCheckRow := container.NewHBox(layout.NewSpacer(), isLowerBoundCheck, isLowerBoundHelpButton, layout.NewSpacer()) diff --git a/gui/helpGui.go b/gui/helpGui.go index 38e8202..369275e 100644 --- a/gui/helpGui.go +++ b/gui/helpGui.go @@ -674,45 +674,95 @@ func setPolygenicDiseaseLocusRiskWeightProbabilityExplainerPage(window fyne.Wind } -func setTraitOutcomeScoresExplainerPage(window fyne.Window, previousPage func()){ +func setDiscreteTraitNeuralNetworkPredictionExplainerPage(window fyne.Window, previousPage func()){ - title := getPageTitleCentered("Help - Outcome Scores") + title := getPageTitleCentered("Help - Neural Network Prediction") backButton := getBackButtonCentered(previousPage) - subtitle := getPageSubtitleCentered("Trait Outcome Scores") + subtitle := getPageSubtitleCentered("Discrete Trait Neural Network Prediction") - description1 := getLabelCentered("Person genetic analyses contain trait analyses.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") - description3 := getLabelCentered("Points are added to an outcome to represent a higher probability.") - description4 := getLabelCentered("Points are subtracted from an outcome to represent a lower probability.") - description5 := getLabelCentered("The more rules that are tested, the higher the accuracy of the result will be.") + description1 := getLabelCentered("Person genetic analyses contain discrete trait analyses.") + description2 := getLabelCentered("There are 2 discrete trait analysis methods: Neural Networks and Rules.") + description3 := getLabelCentered("Each trait can be analyzed by either rules or a neural network.") + description4 := getLabelCentered("Neural network prediction is calculated by inputing a genome's loci into a neural network.") + description5 := getLabelCentered("Each trait has multiple outcomes, and the neural network predicts a single outcome.") + description6 := getLabelCentered("The higher the quantity of tested loci, the more accurate the result is.") + description7 := getLabelCentered("The probability that a neural network's prediction is accurate is called its Confidence.") - page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) setPageContent(page, window) } -func setOffspringTraitOutcomeScoresExplainerPage(window fyne.Window, previousPage func()){ +func setOffspringDiscreteTraitNeuralNetworkPredictionExplainerPage(window fyne.Window, previousPage func()){ - title := getPageTitleCentered("Help - Outcome Scores") + title := getPageTitleCentered("Help - Neural Network Prediction") backButton := getBackButtonCentered(previousPage) - subtitle := getPageSubtitleCentered("Offspring Trait Outcome Scores") + subtitle := getPageSubtitleCentered("Offspring Discrete Trait Neural Network Prediction") - description1 := getLabelCentered("Couple genetic analyses contain trait analyses for the offspring.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") - description3 := getLabelCentered("Points are added to an outcome to represent a higher probability.") - description4 := getLabelCentered("Points are subtracted from an outcome to represent a lower probability.") - description5 := getLabelCentered("The more rules that are tested, the higher the accuracy of the result will be.") + description1 := getLabelCentered("Couple genetic analyses contain discrete trait analyses.") + description2 := getLabelCentered("There are 2 discrete trait analysis methods: Neural Networks and Rules.") + description3 := getLabelCentered("Each trait can be analyzed by either rules or a neural network.") + description4 := getLabelCentered("Neural network prediction is calculated by inputing a genome's loci into a neural network.") + description5 := getLabelCentered("Each trait has multiple outcomes, and the neural network predicts the probability of each outcome.") + description6 := getLabelCentered("The higher the quantity of known loci, the more accurate the result is.") + description7 := getLabelCentered("The probability that a neural network's prediction is accurate is called its Confidence.") + description8 := getLabelCentered("For couples, the Confidence is the average of the confidence of 100 prospective offspring predictions.") - page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8) setPageContent(page, window) } -func setTraitRulesExplainerPage(window fyne.Window, previousPage func()){ +func setDiscreteTraitRulesPredictionExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Rules Prediction") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Discrete Trait Rules Prediction") + + description1 := getLabelCentered("Person genetic analyses contain discrete trait analyses.") + description2 := getLabelCentered("There are 2 discrete trait analysis methods: Neural Networks and Rules.") + description3 := getLabelCentered("Each trait can be analyzed by either rules or a neural network.") + description4 := getLabelCentered("Rule prediction is calculated by testing many predictive rules.") + description5 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description6 := getLabelCentered("Points are added to an outcome to represent a higher probability.") + description7 := getLabelCentered("Points are subtracted from an outcome to represent a lower probability.") + description8 := getLabelCentered("The more rules that are tested, the higher the accuracy of the result will be.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8) + + setPageContent(page, window) +} + +func setOffspringDiscreteTraitRulesPredictionExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Rules Prediction") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Discrete Trait Rules Prediction") + + description1 := getLabelCentered("Couple genetic analyses contain discrete trait analyses for the offspring.") + description2 := getLabelCentered("There are 2 discrete trait analysis methods: Neural Networks and Rules.") + description3 := getLabelCentered("Each trait can be analyzed by either rules or a neural network.") + description4 := getLabelCentered("Rule prediction is calculated by testing many predictive rules.") + description5 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description6 := getLabelCentered("Points are added to an outcome to represent a higher probability.") + description7 := getLabelCentered("Points are subtracted from an outcome to represent a lower probability.") + description8 := getLabelCentered("The more rules that are tested, the higher the accuracy of the result will be.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8) + + setPageContent(page, window) +} + + +func setDiscreteTraitRulesExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - Trait Rules") @@ -720,8 +770,8 @@ func setTraitRulesExplainerPage(window fyne.Window, previousPage func()){ subtitle := getPageSubtitleCentered("Trait Rules") - description1 := getLabelCentered("Person genetic analyses contain trait analyses.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description1 := getLabelCentered("Person genetic analyses contain discrete trait analyses.") + description2 := getLabelCentered("Discrete traits has multiple outcomes, and each outcome has an associated score.") description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") description4 := getLabelCentered("Each outcome's score is determined by trait rules.") description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") @@ -732,16 +782,16 @@ func setTraitRulesExplainerPage(window fyne.Window, previousPage func()){ setPageContent(page, window) } -func setOffspringTraitRulesExplainerPage(window fyne.Window, previousPage func()){ +func setOffspringDiscreteTraitRulesExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - Trait Rules") backButton := getBackButtonCentered(previousPage) - subtitle := getPageSubtitleCentered("Trait Rules") + subtitle := getPageSubtitleCentered("Discrete Trait Rules") - description1 := getLabelCentered("Offspring genetic analyses contain trait analyses for a couple's offspring.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description1 := getLabelCentered("Offspring genetic analyses contain discrete trait analyses for a couple's offspring.") + description2 := getLabelCentered("Each discrete trait has multiple outcomes, and each outcome has an associated score.") description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") description4 := getLabelCentered("Each outcome's score is determined by trait rules.") description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") @@ -753,15 +803,15 @@ func setOffspringTraitRulesExplainerPage(window fyne.Window, previousPage func() setPageContent(page, window) } -func setTraitNumberOfRulesTestedExplainerPage(window fyne.Window, previousPage func()){ +func setDiscreteTraitQuantityOfRulesTestedExplainerPage(window fyne.Window, previousPage func()){ - title := getPageTitleCentered("Help - Number Of Rules Tested") + title := getPageTitleCentered("Help - Quantity Of Rules Tested") backButton := getBackButtonCentered(previousPage) - subtitle := getPageSubtitleCentered("Number Of Trait Rules Tested") + subtitle := getPageSubtitleCentered("Quantity Of Trait Rules Tested") - description1 := getLabelCentered("Person genetic analyses contain trait analyses.") + description1 := getLabelCentered("Person genetic analyses contain discrete trait analyses.") description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represent the opposite.") description4 := getLabelCentered("Each outcome's score is determined by trait rules.") @@ -775,13 +825,13 @@ func setTraitNumberOfRulesTestedExplainerPage(window fyne.Window, previousPage f setPageContent(page, window) } -func setOffspringTraitNumberOfRulesTestedExplainerPage(window fyne.Window, previousPage func()){ +func setOffspringDiscreteTraitQuantityOfRulesTestedExplainerPage(window fyne.Window, previousPage func()){ - title := getPageTitleCentered("Help - Number Of Rules Tested") + title := getPageTitleCentered("Help - Quantity Of Rules Tested") backButton := getBackButtonCentered(previousPage) - subtitle := getPageSubtitleCentered("Offspring Trait Number Of Rules Tested") + subtitle := getPageSubtitleCentered("Offspring Trait Quantity Of Rules Tested") description1 := getLabelCentered("Offspring genetic analyses contain trait analyses for a couple's offspring.") description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") @@ -806,8 +856,8 @@ func setOffspringProbabilityOfPassingTraitRuleExplainerPage(window fyne.Window, subtitle := getPageSubtitleCentered("Offspring Probability Of Passing Trait Rule") - description1 := getLabelCentered("Offspring genetic analyses contain trait analyses for a couple's offspring.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description1 := getLabelCentered("Offspring genetic analyses contain discrete trait analyses for a couple's offspring.") + description2 := getLabelCentered("Each discrete trait has multiple outcomes, and each outcome has an associated score.") description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") description4 := getLabelCentered("Each outcome's score is determined by trait rules.") description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") @@ -821,8 +871,7 @@ func setOffspringProbabilityOfPassingTraitRuleExplainerPage(window fyne.Window, setPageContent(page, window) } - -func setPersonPassesTraitRuleExplainerPage(window fyne.Window, previousPage func()){ +func setPersonPassesDiscreteTraitRuleExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - Trait Rules") @@ -830,8 +879,8 @@ func setPersonPassesTraitRuleExplainerPage(window fyne.Window, previousPage func subtitle := getPageSubtitleCentered("Person Passes Trait Rule") - description1 := getLabelCentered("Person genetic analyses contain trait analyses.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description1 := getLabelCentered("Person genetic analyses contain discrete trait analyses.") + description2 := getLabelCentered("Each discrete trait has multiple outcomes, and each outcome has an associated score.") description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") description4 := getLabelCentered("Each outcome's score is determined by trait rules.") description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") @@ -842,7 +891,7 @@ func setPersonPassesTraitRuleExplainerPage(window fyne.Window, previousPage func setPageContent(page, window) } -func setGenomePassesTraitRuleExplainerPage(window fyne.Window, previousPage func()){ +func setGenomePassesDiscreteTraitRuleExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - Genome Passes Rule") @@ -850,22 +899,23 @@ func setGenomePassesTraitRuleExplainerPage(window fyne.Window, previousPage func subtitle := getPageSubtitleCentered("Genome Passes Trait Rule") - description1 := getLabelCentered("Person genetic analyses contain trait analyses.") - description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") - description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") - description4 := getLabelCentered("Each outcome's score is determined by trait rules.") - description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") - description6 := getLabelCentered("If a person passes a rule, its effects will be applied to their outcome(s).") - description7 := getLabelCentered("If a person has imported multiple genomes, each genome may or may not pass the rule.") - description8 := getLabelCentered("If one genome passes and another does not, it means that one genome has an invalid value.") - description9 := getLabelCentered("This happens because genome sequencing technology is not perfectly accurate.") + description1 := getLabelCentered("Person genetic analyses contain discrete trait analyses.") + description2 := getLabelCentered("There are 2 discrete trait analysis methods: Rules and Neural Networks.") + description3 := getLabelCentered("For rule-based analyses, each trait's outcome has an associated score.") + description4 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description5 := getLabelCentered("Each outcome's score is determined by trait rules.") + description6 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description7 := getLabelCentered("If a person passes a rule, its effects will be applied to their outcome(s).") + description8 := getLabelCentered("If a person has imported multiple genomes, each genome may or may not pass the rule.") + description9 := getLabelCentered("If one genome passes and another does not, it means that one genome has an invalid value.") + description10 := getLabelCentered("This happens because genome sequencing technology is not perfectly accurate.") - page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, description9) + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, description9, description10) setPageContent(page, window) } -func setTraitRuleOutcomeEffectsExplainerPage(window fyne.Window, previousPage func()){ +func setDiscreteTraitRuleOutcomeEffectsExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - Outcome Effects") @@ -885,7 +935,7 @@ func setTraitRuleOutcomeEffectsExplainerPage(window fyne.Window, previousPage fu } -func setWealthOrIncomeIsLowerBoundExplainerPage(window fyne.Window, previousPage func()){ +func setWealthIsLowerBoundExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - Is Lower Bound") @@ -905,7 +955,7 @@ func setWealthOrIncomeIsLowerBoundExplainerPage(window fyne.Window, previousPage func setMemoExplainerPage(window fyne.Window, previousPage func()){ - + title := getPageTitleCentered("Help - Memo") backButton := getBackButtonCentered(previousPage) @@ -1026,7 +1076,7 @@ func setHostingHelpPage(window fyne.Window, previousPage func()){ description1 := getLabelCentered("Be a Seekia host.") description2 := getLabelCentered("Your computer will seed content to the network.") description3 := getLabelCentered("You can choose to host as much or as little as you desire.") - + //TODO: Add buttons to show more help //TODO: Describe how hosting works and associated risks diff --git a/gui/viewAnalysisGui_Couple.go b/gui/viewAnalysisGui_Couple.go index 2cc043f..07a59db 100644 --- a/gui/viewAnalysisGui_Couple.go +++ b/gui/viewAnalysisGui_Couple.go @@ -10,6 +10,7 @@ import "fyne.io/fyne/v2/theme" import "fyne.io/fyne/v2/widget" import "fyne.io/fyne/v2/canvas" +import "seekia/resources/geneticPredictionModels" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" @@ -48,7 +49,7 @@ func setViewCoupleGeneticAnalysisPage(window fyne.Window, person1Identifier stri return } if (person1Found == false){ - setErrorEncounteredPage(window, errors.New("Couple person A not found."), previousPage) + setErrorEncounteredPage(window, errors.New("Couple person 1 not found."), previousPage) return } @@ -58,7 +59,7 @@ func setViewCoupleGeneticAnalysisPage(window fyne.Window, person1Identifier stri return } if (person2Found == false){ - setErrorEncounteredPage(window, errors.New("Couple person B not found."), previousPage) + setErrorEncounteredPage(window, errors.New("Couple person 2 not found."), previousPage) return } @@ -88,11 +89,15 @@ func setViewCoupleGeneticAnalysisPage(window fyne.Window, person1Identifier stri polygenicDiseasesButton := widget.NewButton("Polygenic Diseases", func(){ setViewCoupleGeneticAnalysisPolygenicDiseasesPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, currentPage) }) - traitsButton := widget.NewButton("Traits", func(){ - setViewCoupleGeneticAnalysisTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, currentPage) + discreteTraitsButton := widget.NewButton("Discrete Traits", func(){ + setViewCoupleGeneticAnalysisDiscreteTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, currentPage) + }) + numericTraitsButton := widget.NewButton("Numeric Traits", func(){ + //TODO + showUnderConstructionDialog(window) }) - categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, traitsButton)) + categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, discreteTraitsButton, numericTraitsButton)) page := container.NewVBox(title, backButton, widget.NewSeparator(), warningLabel1, warningLabel2, widget.NewSeparator(), coupleNameRow, widget.NewSeparator(), numberOfAnalyzedGenomesLabel, person1NumberOfAnalyzedGenomesRow, person2NumberOfAnalyzedGenomesRow, widget.NewSeparator(), categoryButtonsGrid) @@ -1327,7 +1332,7 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window fy genomeNameLabel := getBoldLabelCentered(genomeName) - personRiskScoreKnown, _, personRiskScoreFormatted, _, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, personAnalysisGenomeIdentifier) + personRiskScoreKnown, _, personRiskScoreFormatted, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, personAnalysisGenomeIdentifier) if (err != nil) { return err } getPersonRiskScoreLabelText := func()string{ @@ -1860,120 +1865,206 @@ func setViewPolygenicDiseaseSampleOffspringRiskScoresChart(window fyne.Window, d } -func setViewCoupleGeneticAnalysisTraitsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, previousPage func()){ +func setViewCoupleGeneticAnalysisDiscreteTraitsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, previousPage func()){ - currentPage := func(){setViewCoupleGeneticAnalysisTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, previousPage)} + currentPage := func(){setViewCoupleGeneticAnalysisDiscreteTraitsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, previousPage)} - title := getPageTitleCentered("Viewing Genetic Analysis - Traits") + title := getPageTitleCentered("Viewing Genetic Analysis - Discrete Traits") backButton := getBackButtonCentered(previousPage) - description := getLabelCentered("Below is an analysis of the average trait scores for the couple's offspring.") + description := getLabelCentered("Below is an analysis of the average discrete 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") - offspringOutcomeScoresLabel := getItalicLabelCentered("Offspring Outcome Scores") + emptyLabel2 := widget.NewLabel("") + predictedProbabilitiesLabel := getItalicLabelCentered("Predicted Probabilities") + quantityOfLabel := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + emptyLabel3 := widget.NewLabel("") conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") - emptyLabel := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") - traitNameColumn := container.NewVBox(traitNameLabel, widget.NewSeparator()) - offspringOutcomeScoresColumn := container.NewVBox(offspringOutcomeScoresLabel, widget.NewSeparator()) - conflictExistsColumn := container.NewVBox(conflictExistsLabel, widget.NewSeparator()) - viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + traitNameColumn := container.NewVBox(emptyLabel1, traitNameLabel, widget.NewSeparator()) + predictedProbabilitiesColumn := container.NewVBox(emptyLabel2, predictedProbabilitiesLabel, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel, lociKnownLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(emptyLabel3, conflictExistsLabel, widget.NewSeparator()) + viewDetailsButtonsColumn := 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 != "Discrete"){ + continue + } + traitLociList := traitObject.LociList traitRulesList := traitObject.RulesList - if (len(traitRulesList) == 0){ + if (len(traitLociList) == 0 && len(traitRulesList) == 0){ // This trait does not have any rules // We cannot analyze it yet // We will add neural network prediction so we can predict these traits continue } - mainGenomePairIdentifier := helpers.JoinTwo16ByteArrays(pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier) + traitName := traitObject.TraitName - offspringOutcomeScoresKnown, offspringAverageOutcomeScoresMap, _, conflictExists, err := readGeneticAnalysis.GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, mainGenomePairIdentifier) + neuralNetworkExists, neuralNetworkAnalysisExists, offspringOutcomeProbabilitiesMap_NeuralNetwork, _, quantityOfLociKnown_NeuralNetwork, _, anyRulesExist, rulesAnalysisExists, offspringOutcomeProbabilitiesMap_Rules, _, _, quantityOfLociKnown_Rules, conflictExists, err := readGeneticAnalysis.GetOffspringDiscreteTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, mainGenomePairIdentifier) if (err != nil) { return nil, err } + if (neuralNetworkExists == false && anyRulesExist == false){ + // We cannot analyze this trait + continue + } - // We add all of the columns except for the trait outcomes column, which may be multiple rows high + getQuantityOfLociKnown := func()int{ + if (neuralNetworkExists == true){ + return quantityOfLociKnown_NeuralNetwork + } + return quantityOfLociKnown_Rules + } + + quantityOfLociKnown := getQuantityOfLociKnown() + + getTotalQuantityOfLoci := func()int{ + + if (neuralNetworkExists == true){ + + totalQuantityOfLoci := len(traitLociList) + + return totalQuantityOfLoci + } + + traitLociList_Rules := traitObject.LociList_Rules + + totalQuantityOfLoci := len(traitLociList_Rules) + + return totalQuantityOfLoci + } + + totalQuantityOfLoci := getTotalQuantityOfLoci() + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + // We add each row except for the outcome rows + // The outcome grid cell can be multiple rows tall + + traitNameLabel := getBoldLabelCentered(traitName) - traitNameText := getBoldLabelCentered(traitName) - conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) conflictExistsLabel := getBoldLabelCentered(conflictExistsString) - + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewCoupleGeneticAnalysisTraitDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, currentPage) + setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, currentPage) })) - traitNameColumn.Add(traitNameText) + traitNameColumn.Add(traitNameLabel) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) conflictExistsColumn.Add(conflictExistsLabel) - viewButtonsColumn.Add(viewDetailsButton) - - if (offspringOutcomeScoresKnown == false){ + viewDetailsButtonsColumn.Add(viewDetailsButton) - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) + // Outputs: + // -bool: Outcome probabilities exist + // -map[string]int: Outcome Name -> Probability of outcome (0-100) + getOutcomeProbabilitiesMap := func()(bool, map[string]int){ - offspringOutcomeScoresColumn.Add(unknownLabel) - } else { + if (neuralNetworkExists == true){ - outcomeNamesList := helpers.GetListOfMapKeys(offspringAverageOutcomeScoresMap) - - // We have to sort outcome names so they always show up in the same order - helpers.SortStringListToUnicodeOrder(outcomeNamesList) - - for index, outcomeName := range outcomeNamesList{ - - outcomeScore, exists := offspringAverageOutcomeScoresMap[outcomeName] - if (exists == false){ - return nil, errors.New("Outcome name not found in outcomeScoresMap after being found already.") + if (neuralNetworkAnalysisExists == false){ + return false, nil } - outcomeScoreString := helpers.ConvertFloat64ToStringRounded(outcomeScore, 2) + return true, offspringOutcomeProbabilitiesMap_NeuralNetwork + } - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - offspringOutcomeScoresColumn.Add(outcomeRow) + //anyRulesExist must be true + if (rulesAnalysisExists == false){ + return false, nil + } + return true, offspringOutcomeProbabilitiesMap_Rules + } - if (index > 0){ + outcomeProbabilitiesExist, outcomeProbabilitiesMap := getOutcomeProbabilitiesMap() + if (outcomeProbabilitiesExist == false){ + unknownLabel := getItalicLabelCentered("Unknown") + predictedProbabilitiesColumn.Add(unknownLabel) + } else { - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") + outcomeNamesList := traitObject.OutcomesList - traitNameColumn.Add(emptyLabelA) - conflictExistsColumn.Add(emptyLabelB) - viewButtonsColumn.Add(emptyLabelC) + outcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(outcomeNamesList) + + addedItems := 0 + + for _, outcomeName := range outcomeNamesListSorted{ + + outcomeProbability, exists := outcomeProbabilitiesMap[outcomeName] + if (exists == false){ + continue + } + + if (outcomeProbability == 0){ + continue + } + + outcomeProbabilityString := helpers.ConvertIntToString(outcomeProbability) + + outcomeRowLabel := getBoldLabel(outcomeName + ": " + outcomeProbabilityString + "%") + + predictedProbabilitiesColumn.Add(outcomeRowLabel) + + addedItems += 1 + + if (addedItems != 1){ + // We have to add whitespace to the other columns + traitNameColumn.Add(widget.NewLabel("")) + quantityOfLociKnownColumn.Add(widget.NewLabel("")) + conflictExistsColumn.Add(widget.NewLabel("")) + viewDetailsButtonsColumn.Add(widget.NewLabel("")) } } } traitNameColumn.Add(widget.NewSeparator()) - offspringOutcomeScoresColumn.Add(widget.NewSeparator()) + predictedProbabilitiesColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) conflictExistsColumn.Add(widget.NewSeparator()) - viewButtonsColumn.Add(widget.NewSeparator()) + viewDetailsButtonsColumn.Add(widget.NewSeparator()) } - offspringOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + predictedProbabilitiesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) }) - offspringOutcomeScoresColumn.Add(offspringOutcomeScoresHelpButton) + predictedProbabilitiesColumn.Add(predictedProbabilitiesHelpButton) - traitsGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, offspringOutcomeScoresColumn) + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) + + traitsGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, predictedProbabilitiesColumn, quantityOfLociKnownColumn) if (secondGenomePairExists == true){ @@ -1985,7 +2076,7 @@ func setViewCoupleGeneticAnalysisTraitsPage(window fyne.Window, person1Name stri traitsGrid.Add(conflictExistsColumn) } - traitsGrid.Add(viewButtonsColumn) + traitsGrid.Add(viewDetailsButtonsColumn) traitsGrid.Add(layout.NewSpacer()) return traitsGrid, nil @@ -2003,15 +2094,14 @@ func setViewCoupleGeneticAnalysisTraitsPage(window fyne.Window, person1Name stri } +func setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, previousPage func()){ -func setViewCoupleGeneticAnalysisTraitDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, previousPage func()){ - - currentPage := func(){setViewCoupleGeneticAnalysisTraitDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, previousPage)} + currentPage := func(){setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage(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) @@ -2043,17 +2133,32 @@ func setViewCoupleGeneticAnalysisTraitDetailsPage(window fyne.Window, person1Nam }) traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitNameInfoButton, layout.NewSpacer()) - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) - offspringOutcomeScoresLabel := getItalicLabelCentered("Offspring Outcome Scores") - - emptyLabelC := widget.NewLabel("") + emptyLabel1 := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") - viewGenomePairButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) - pairNameColumn := container.NewVBox(emptyLabelB, widget.NewSeparator()) - offspringOutcomeScoresColumn := container.NewVBox(offspringOutcomeScoresLabel, widget.NewSeparator()) - viewOffspringRulesButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) + emptyLabel3 := widget.NewLabel("") + genomePairLabel := getItalicLabelCentered("Genome Pair") + + emptyLabel4 := widget.NewLabel("") + predictedProbabilitiesLabel := getItalicLabelCentered("Predicted Probabilities") + + predictionLabel := getItalicLabelCentered("Prediction") + confidenceLabel := getItalicLabelCentered("Confidence") + + quantityOfLabel := getItalicLabelCentered("Quantity Of") + rulesTestedLabel := getItalicLabelCentered("Rules Tested") + + emptyLabel5 := widget.NewLabel("") + emptyLabel6 := widget.NewLabel("") + + viewGenomePairButtonsColumn := container.NewVBox(emptyLabel1, emptyLabel2, widget.NewSeparator()) + pairNameColumn := container.NewVBox(emptyLabel3, genomePairLabel, widget.NewSeparator()) + predictedProbabilitiesColumn := container.NewVBox(emptyLabel4, predictedProbabilitiesLabel, widget.NewSeparator()) + neuralNetworkPredictionConfidenceColumn := container.NewVBox(predictionLabel, confidenceLabel, widget.NewSeparator()) + quantityOfRulesTestedColumn := container.NewVBox(quantityOfLabel, rulesTestedLabel, widget.NewSeparator()) + viewDetailsButtonsColumn := container.NewVBox(emptyLabel5, emptyLabel6, widget.NewSeparator()) addGenomePairRow := func(genomePairName string, person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ @@ -2062,65 +2167,143 @@ func setViewCoupleGeneticAnalysisTraitDetailsPage(window fyne.Window, person1Nam genomePairNameLabel := getBoldLabelCentered(genomePairName) viewGenomePairButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) + setViewCoupleGeneticAnalysisDiscreteTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) }) - - viewOffspringRulesButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewCoupleTraitRulesPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) + viewAnalysisDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + if (neuralNetworkExists == true){ + //TODO + showUnderConstructionDialog(window) + } else { + setViewCoupleDiscreteTraitRulesPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) + } }) - // We add all of the columns except for the trait rule column, which may be multiple rows high - viewGenomePairButtonsColumn.Add(viewGenomePairButton) pairNameColumn.Add(genomePairNameLabel) - viewOffspringRulesButtonsColumn.Add(viewOffspringRulesButton) + viewDetailsButtonsColumn.Add(viewAnalysisDetailsButton) - offspringOutcomeScoresKnown, offspringOutcomeScoresMap, _, _, err := readGeneticAnalysis.GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) + traitObject, err := traits.GetTraitObject(traitName) if (err != nil) { return err } - if (offspringOutcomeScoresKnown == false){ - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ + return errors.New("setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage called with non-discrete trait: " + traitName) + } - offspringOutcomeScoresColumn.Add(unknownLabel) + 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){ + return errors.New("setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage called with trait that is not analyzable.") + } + + // First we add analysis details to their respective column + + if (neuralNetworkExists == true){ + + if (neuralNetworkAnalysisExists == false){ + + neuralNetworkPredictionConfidenceColumn.Add(getLabelCentered("-")) + } else { + + predictionConfidenceString := helpers.ConvertIntToString(neuralNetworkPredictionConfidence) + + predictionConfidenceLabel := getBoldLabelCentered(predictionConfidenceString + "%") + + neuralNetworkPredictionConfidenceColumn.Add(predictionConfidenceLabel) + } } else { + if (anyRulesExist == false){ + return errors.New("setViewCoupleGeneticAnalysisDiscreteTraitDetailsPage called with analysis which is missing ") + } - outcomeNamesList := helpers.GetListOfMapKeys(offspringOutcomeScoresMap) + traitRulesList := traitObject.RulesList - // We have to sort the outcome names so they always show up in the same order - helpers.SortStringListToUnicodeOrder(outcomeNamesList) + totalNumberOfRules := len(traitRulesList) - for index, outcomeName := range outcomeNamesList{ + totalNumberOfRulesString := helpers.ConvertIntToString(totalNumberOfRules) - outcomeScore, exists := offspringOutcomeScoresMap[outcomeName] - if (exists == false){ - return errors.New("Outcome name not found in outcome scores map after being found already.") + quantityOfRulesTestedString := helpers.ConvertIntToString(quantityOfRulesTested) + + quantityOfRulesTestedFormatted := quantityOfRulesTestedString + "/" + totalNumberOfRulesString + + quantityOfRulesTestedLabel := getBoldLabelCentered(quantityOfRulesTestedFormatted) + + quantityOfRulesTestedColumn.Add(quantityOfRulesTestedLabel) + } + + // Now we add the outcome probabilities cell + + // Outputs: + // -bool: Outcome probabilities exist + // -map[string]int: Outcome Name -> Probability of outcome (0-100) + getOutcomeProbabilitiesMap := func()(bool, map[string]int){ + + if (neuralNetworkExists == true){ + + if (neuralNetworkAnalysisExists == false){ + return false, nil } - - outcomeScoreString := helpers.ConvertFloat64ToStringRounded(outcomeScore, 2) - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - offspringOutcomeScoresColumn.Add(outcomeRow) + return true, offspringOutcomeProbabilitiesMap_NeuralNetwork + } - if (index > 0){ - - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") + //anyRulesExist must be true + if (rulesAnalysisExists == false){ + return false, nil + } + return true, offspringOutcomeProbabilitiesMap_Rules + } - pairNameColumn.Add(emptyLabelA) - viewGenomePairButtonsColumn.Add(emptyLabelB) - viewOffspringRulesButtonsColumn.Add(emptyLabelC) + outcomeProbabilitiesExist, outcomeProbabilitiesMap := getOutcomeProbabilitiesMap() + if (outcomeProbabilitiesExist == false){ + unknownLabel := getItalicLabelCentered("Unknown") + predictedProbabilitiesColumn.Add(unknownLabel) + } else { + + outcomeNamesList := traitObject.OutcomesList + + outcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(outcomeNamesList) + + addedItems := 0 + + for _, outcomeName := range outcomeNamesListSorted{ + + outcomeProbability, exists := outcomeProbabilitiesMap[outcomeName] + if (exists == false){ + continue + } + + if (outcomeProbability == 0){ + continue + } + + outcomeProbabilityString := helpers.ConvertIntToString(outcomeProbability) + + outcomeRowLabel := getBoldLabel(outcomeName + ": " + outcomeProbabilityString + "%") + + predictedProbabilitiesColumn.Add(outcomeRowLabel) + + addedItems += 1 + + if (addedItems > 1){ + // We have to add whitespace to the other columns + viewGenomePairButtonsColumn.Add(widget.NewLabel("")) + pairNameColumn.Add(widget.NewLabel("")) + neuralNetworkPredictionConfidenceColumn.Add(widget.NewLabel("")) + quantityOfRulesTestedColumn.Add(widget.NewLabel("")) + viewDetailsButtonsColumn.Add(widget.NewLabel("")) } } } viewGenomePairButtonsColumn.Add(widget.NewSeparator()) pairNameColumn.Add(widget.NewSeparator()) - offspringOutcomeScoresColumn.Add(widget.NewSeparator()) - viewOffspringRulesButtonsColumn.Add(widget.NewSeparator()) + predictedProbabilitiesColumn.Add(widget.NewSeparator()) + neuralNetworkPredictionConfidenceColumn.Add(widget.NewSeparator()) + quantityOfRulesTestedColumn.Add(widget.NewSeparator()) + viewDetailsButtonsColumn.Add(widget.NewSeparator()) return nil } @@ -2139,23 +2322,49 @@ func setViewCoupleGeneticAnalysisTraitDetailsPage(window fyne.Window, person1Nam } } - offspringOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + predictedProbabilitiesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) }) - offspringOutcomeScoresColumn.Add(offspringOutcomeScoresHelpButton) + predictedProbabilitiesColumn.Add(predictedProbabilitiesHelpButton) - genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, offspringOutcomeScoresColumn, viewOffspringRulesButtonsColumn, layout.NewSpacer()) + if (neuralNetworkExists == true){ + + neuralNetworkPredictionConfidenceHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringDiscreteTraitNeuralNetworkPredictionExplainerPage(window, currentPage) + }) + + neuralNetworkPredictionConfidenceColumn.Add(neuralNetworkPredictionConfidenceHelpButton) + } else { + quantityOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) + }) + + quantityOfRulesTestedColumn.Add(quantityOfRulesTestedHelpButton) + } + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, predictedProbabilitiesColumn) + + if (neuralNetworkExists == true){ + + genomesContainer.Add(neuralNetworkPredictionConfidenceColumn) + } else { + genomesContainer.Add(quantityOfRulesTestedColumn) + } + + genomesContainer.Add(viewDetailsButtonsColumn) + genomesContainer.Add(layout.NewSpacer()) page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), genomesContainer) - + setPageContent(page, window) } -func setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte, genomePairName string, previousPage func()){ +func setViewCoupleGeneticAnalysisDiscreteTraitGenomePairDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte, genomePairName string, previousPage func()){ - currentPage := func(){setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, previousPage)} + currentPage := func(){setViewCoupleGeneticAnalysisDiscreteTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, previousPage)} title := getPageTitleCentered("Viewing Couple Genome Pair Info") @@ -2177,26 +2386,30 @@ func setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window fyne.Window, }) genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, genomePairHelpButton, layout.NewSpacer()) - emptyLabelA := widget.NewLabel("") + emptyLabel1 := widget.NewLabel("") personNameLabel := getItalicLabelCentered("Person Name") - emptyLabelB := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") genomeNameLabel := getItalicLabelCentered("Genome Name") - emptyLabelC := widget.NewLabel("") - outcomeScoresLabel := getItalicLabelCentered("Outcome Scores") + emptyLabel3 := widget.NewLabel("") + predictedOutcomeLabel := getItalicLabelCentered("Predicted Outcome") - numberOfLabel := getItalicLabelCentered("Number Of") + quantityOfLabel1 := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + quantityOfLabel2 := getItalicLabelCentered("Quantity Of") rulesTestedLabel := getItalicLabelCentered("Rules Tested") - emptyLabelD := widget.NewLabel("") - emptyLabelE := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") - personNameColumn := container.NewVBox(emptyLabelA, personNameLabel, widget.NewSeparator()) - genomeNameColumn := container.NewVBox(emptyLabelB, genomeNameLabel, widget.NewSeparator()) - outcomeScoresColumn := container.NewVBox(emptyLabelC, outcomeScoresLabel, widget.NewSeparator()) - numberOfRulesTestedColumn := container.NewVBox(numberOfLabel, rulesTestedLabel, widget.NewSeparator()) - viewGenomeButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + personNameColumn := container.NewVBox(emptyLabel1, personNameLabel, widget.NewSeparator()) + genomeNameColumn := container.NewVBox(emptyLabel2, genomeNameLabel, widget.NewSeparator()) + predictedOutcomeColumn := container.NewVBox(emptyLabel3, predictedOutcomeLabel, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel1, lociKnownLabel, widget.NewSeparator()) + quantityOfRulesTestedColumn := container.NewVBox(quantityOfLabel2, rulesTestedLabel, widget.NewSeparator()) + viewGenomeButtonsColumn := container.NewVBox(emptyLabel4, emptyLabel5, widget.NewSeparator()) addGenomeRow := func(isPerson1 bool, personName string, inputGenomeIdentifier [16]byte)error{ @@ -2238,68 +2451,103 @@ func setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window fyne.Window, genomeNameLabel := getBoldLabelCentered(genomeName) - // We add all of the columns except for the trait rule column, which may be multiple rows high - - _, anyTraitRuleTested, outcomeScoresMap, numberOfRulesTested, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(personAnalysisObject, traitName, personAnalysisGenomeIdentifier) + neuralNetworkExists, neuralNetworkAnalysisExists, neuralNetworkPredictedOutcome, _, neuralNetworkQuantityOfLociTested, _, _, rulesAnalysisExists, _, rulesPredictedOutcomeExists, rulesPredictedOutcome, quantityOfRulesTested, _, _, err := readGeneticAnalysis.GetPersonDiscreteTraitInfoFromGeneticAnalysis(personAnalysisObject, traitName, personAnalysisGenomeIdentifier) if (err != nil) { return err } - numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) - numberOfRulesTestedText := getBoldLabelCentered(numberOfRulesTestedString) - - viewGenomeButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewPersonGenomeTraitRulesPage(window, personAnalysisObject, traitName, personAnalysisGenomeIdentifier, genomeName, currentPage) - }) personNameColumn.Add(personNameLabel) genomeNameColumn.Add(genomeNameLabel) - numberOfRulesTestedColumn.Add(numberOfRulesTestedText) - viewGenomeButtonsColumn.Add(viewGenomeButton) - if (anyTraitRuleTested == false){ - - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) + //Outputs: + // -bool: Outcome is known + // -string: Prediction outcome + getPredictedOutcome := func()(bool, string){ + if (neuralNetworkExists == true){ - outcomeScoresColumn.Add(unknownLabel) + if (neuralNetworkAnalysisExists == false){ + return false, "" + } + + return true, neuralNetworkPredictedOutcome + } + + // Analysis must be rule-based + if (rulesAnalysisExists == false || rulesPredictedOutcomeExists == false){ + return false, "" + } + + return true, rulesPredictedOutcome + } + + predictedOutcomeExists, predictedOutcome := getPredictedOutcome() + if (predictedOutcomeExists == false){ + unknownLabel := getItalicLabelCentered(translate("Unknown")) + predictedOutcomeColumn.Add(unknownLabel) + } else { + predictedOutcomeLabel := getBoldLabelCentered(predictedOutcome) + predictedOutcomeColumn.Add(predictedOutcomeLabel) + } + + // Now we add outcome details + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return err } + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ + return errors.New("setViewCoupleGeneticAnalysisDiscreteTraitGenomePairDetailsPage called with non-discrete trait: " + traitName) + } + + if (neuralNetworkExists == true){ + + traitLociList := traitObject.LociList + + totalQuantityOfLoci := len(traitLociList) + + quantityOfLociKnownString := helpers.ConvertIntToString(neuralNetworkQuantityOfLociTested) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) } else { - outcomeNamesList := helpers.GetListOfMapKeys(outcomeScoresMap) + // Analysis is rule-based - // We have to sort the outcome names so they always show up in the same order - helpers.SortStringListToUnicodeOrder(outcomeNamesList) + traitRulesList := traitObject.RulesList - for index, outcomeName := range outcomeNamesList{ + totalQuantityOfRules := len(traitRulesList) - outcomeScore, exists := outcomeScoresMap[outcomeName] - if (exists == false){ - return errors.New("Outcome name not found in outcome scores map after being found already.") - } + quantityOfRulesTestedString := helpers.ConvertIntToString(quantityOfRulesTested) + totalQuantityOfRulesString := helpers.ConvertIntToString(totalQuantityOfRules) - outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + quantityOfRulesTestedFormatted := quantityOfRulesTestedString + "/" + totalQuantityOfRulesString - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - outcomeScoresColumn.Add(outcomeRow) + quantityOfRulesTestedLabel := getBoldLabelCentered(quantityOfRulesTestedFormatted) - if (index > 0){ - - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") - emptyLabelD := widget.NewLabel("") - - personNameColumn.Add(emptyLabelA) - genomeNameColumn.Add(emptyLabelB) - numberOfRulesTestedColumn.Add(emptyLabelC) - viewGenomeButtonsColumn.Add(emptyLabelD) - } - } + quantityOfRulesTestedColumn.Add(quantityOfRulesTestedLabel) } + viewGenomeButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + + if (neuralNetworkExists == true){ + //TODO + showUnderConstructionDialog(window) + } else { + setViewPersonGenomeDiscreteTraitRulesPage(window, personAnalysisObject, traitName, personAnalysisGenomeIdentifier, genomeName, currentPage) + } + }) + + viewGenomeButtonsColumn.Add(viewGenomeButton) + personNameColumn.Add(widget.NewSeparator()) genomeNameColumn.Add(widget.NewSeparator()) - outcomeScoresColumn.Add(widget.NewSeparator()) - numberOfRulesTestedColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) + quantityOfRulesTestedColumn.Add(widget.NewSeparator()) viewGenomeButtonsColumn.Add(widget.NewSeparator()) return nil @@ -2318,19 +2566,44 @@ func setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window fyne.Window, return } - outcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + if (neuralNetworkExists == true){ + setDiscreteTraitNeuralNetworkPredictionExplainerPage(window, currentPage) + } else { + setOffspringDiscreteTraitNeuralNetworkPredictionExplainerPage(window, currentPage) + } }) - outcomeScoresColumn.Add(outcomeScoresHelpButton) + predictedOutcomeColumn.Add(predictedOutcomeHelpButton) + + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) numberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + setOffspringDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) }) - numberOfRulesTestedColumn.Add(numberOfRulesTestedHelpButton) + quantityOfRulesTestedColumn.Add(numberOfRulesTestedHelpButton) - genomesGrid := container.NewHBox(layout.NewSpacer(), personNameColumn, genomeNameColumn, outcomeScoresColumn, numberOfRulesTestedColumn, viewGenomeButtonsColumn, layout.NewSpacer()) + genomesGrid := container.NewHBox(layout.NewSpacer(), personNameColumn, genomeNameColumn, predictedOutcomeColumn) + + if (neuralNetworkExists == true){ + + genomesGrid.Add(quantityOfLociKnownColumn) + } else { + + genomesGrid.Add(quantityOfRulesTestedColumn) + } + + genomesGrid.Add(viewGenomeButtonsColumn) + genomesGrid.Add(layout.NewSpacer()) page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), genomePairRow, widget.NewSeparator(), genomesGrid) @@ -2338,13 +2611,12 @@ func setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window fyne.Window, } - // This function provides a page to view the couple offspring rule probabilities for a particular genome pair -func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte, genomePairName string, previousPage func()){ +func setViewCoupleDiscreteTraitRulesPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte, genomePairName string, previousPage func()){ setLoadingScreen(window, "Loading Trait Rules", "Loading trait rules...") - currentPage := func(){setViewCoupleTraitRulesPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, previousPage)} + currentPage := func(){setViewCoupleDiscreteTraitRulesPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, previousPage)} title := getPageTitleCentered("View Offspring Trait Rules - " + traitName) @@ -2352,25 +2624,33 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 description1 := widget.NewLabel("Below are the trait rule probabilities for offspring from this genome pair.") traitRulesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitRulesExplainerPage(window, currentPage) + setOffspringDiscreteTraitRulesExplainerPage(window, currentPage) }) description1Row := container.NewHBox(layout.NewSpacer(), description1, traitRulesHelpButton, layout.NewSpacer()) genomePairLabel := widget.NewLabel("Genome Pair:") genomePairNameLabel := getBoldLabel(genomePairName) viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) + setViewCoupleGeneticAnalysisDiscreteTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) }) genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, viewGenomePairInfoButton, layout.NewSpacer()) - _, _, numberOfRulesTested, _, err := readGeneticAnalysis.GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) + neuralNetworkExists, _, _, _, _, _, anyRulesExist, rulesAnalysisExists, _, _, quantityOfRulesTested, _, _, err := readGeneticAnalysis.GetOffspringDiscreteTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } - - numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + if (neuralNetworkExists == true){ + setErrorEncounteredPage(window, errors.New("setViewCoupleTraitRulesPage called when neural network analysis for trait exists."), previousPage) + return + } + if (anyRulesExist == false){ + setErrorEncounteredPage(window, errors.New("GetOffspringTraitInfoFromGeneticAnalysis claiming that no analysis method exists for triat: " + traitName), previousPage) + return + } + + quantityOfRulesTestedString := helpers.ConvertIntToString(quantityOfRulesTested) traitRulesMap, err := traits.GetTraitRulesMap(traitName) if (err != nil){ @@ -2382,9 +2662,9 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 totalNumberOfRulesString := helpers.ConvertIntToString(totalNumberOfRules) rulesTestedLabel := widget.NewLabel("Rules Tested:") - rulesTestedText := getBoldLabel(numberOfRulesTestedString + "/" + totalNumberOfRulesString) + rulesTestedText := getBoldLabel(quantityOfRulesTestedString + "/" + totalNumberOfRulesString) rulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + setOffspringDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) }) rulesTestedRow := container.NewHBox(layout.NewSpacer(), rulesTestedLabel, rulesTestedText, rulesTestedHelpButton, layout.NewSpacer()) @@ -2415,25 +2695,33 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) if (err != nil) { return err } - offspringRuleProbabilityKnown, _, offspringProbabilityOfPassingRuleFormatted, err := readGeneticAnalysis.GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) - if (err != nil) { return err } + getProbabilityOfPassingRuleText := func()(string, error){ - viewRuleDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, ruleIdentifier, currentPage) - }) + if (rulesAnalysisExists == false){ + // No rules were tested + result := translate("Unknown") + return result, nil + } - getProbabilityOfPassingRuleText := func()string{ + offspringRuleProbabilityKnown, _, offspringProbabilityOfPassingRuleFormatted, err := readGeneticAnalysis.GetOffspringDiscreteTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) + if (err != nil) { return "", err } if (offspringRuleProbabilityKnown == false){ result := translate("Unknown") - return result + return result, nil } - return offspringProbabilityOfPassingRuleFormatted + + return offspringProbabilityOfPassingRuleFormatted, nil } - probabilityOfPassingRuleText := getProbabilityOfPassingRuleText() + probabilityOfPassingRuleText, err := getProbabilityOfPassingRuleText() + if (err != nil) { return err } probabilityOfPassingRuleTextLabel := getBoldLabelCentered(probabilityOfPassingRuleText) + viewRuleDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisDiscreteTraitRuleDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, ruleIdentifier, currentPage) + }) + // We add all of the columns except for the rule effects column // We do this because the rule effects column may be multiple rows tall @@ -2477,7 +2765,7 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 ruleEffectsColumn.Add(outcomeRow) if (index > 0){ - + emptyLabelA := widget.NewLabel("") emptyLabelB := widget.NewLabel("") emptyLabelC := widget.NewLabel("") @@ -2504,7 +2792,7 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) if (err != nil) { return nil, err } - offspringRuleProbabilityKnown, _, _, err := readGeneticAnalysis.GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) + offspringRuleProbabilityKnown, _, _, err := readGeneticAnalysis.GetOffspringDiscreteTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) if (err != nil) { return nil, err } if (offspringRuleProbabilityKnown == true){ rulesWithKnownProbabilityList = append(rulesWithKnownProbabilityList, ruleIdentifierHex) @@ -2531,7 +2819,7 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 } ruleEffectsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + setDiscreteTraitRuleOutcomeEffectsExplainerPage(window, currentPage) }) ruleEffectsColumn.Add(ruleEffectsHelpButton) @@ -2539,7 +2827,7 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 setOffspringProbabilityOfPassingTraitRuleExplainerPage(window, currentPage) }) offspringProbabilityOfPassingRuleColumn.Add(offspringProbabilityOfPassingRuleHelpButton) - + rulesGrid := container.NewHBox(layout.NewSpacer(), ruleIdentifierColumn, ruleEffectsColumn, offspringProbabilityOfPassingRuleColumn, ruleInfoButtonsColumn, layout.NewSpacer()) return rulesGrid, nil @@ -2560,9 +2848,9 @@ func setViewCoupleTraitRulesPage(window fyne.Window, person1Name string, person2 // This function implements a page to view the details of a specific rule from a genetic analysis // It will show the rule details for all of the couple's genome pairs -func setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, ruleIdentifier [3]byte, previousPage func()){ +func setViewCoupleGeneticAnalysisDiscreteTraitRuleDetailsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, ruleIdentifier [3]byte, previousPage func()){ - currentPage := func(){setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, ruleIdentifier, previousPage)} + currentPage := func(){setViewCoupleGeneticAnalysisDiscreteTraitRuleDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, ruleIdentifier, previousPage)} title := getPageTitleCentered("Trait Rule Details - " + traitName) @@ -2575,7 +2863,7 @@ func setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, person ruleIdentifierLabel := widget.NewLabel("Rule Identifier:") ruleIdentifierText := getBoldLabel(ruleIdentifierHex) ruleInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewTraitRuleDetailsPage(window, traitName, ruleIdentifierHex, currentPage) + setViewDiscreteTraitRuleDetailsPage(window, traitName, ruleIdentifierHex, currentPage) }) ruleIdentifierRow := container.NewHBox(layout.NewSpacer(), ruleIdentifierLabel, ruleIdentifierText, ruleInfoButton, layout.NewSpacer()) @@ -2596,7 +2884,7 @@ func setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, person addGenomePairRow := func(genomePairName string, genomePairIdentifier [32]byte)error{ - offspringRuleProbabilityKnown, _, offspringProbabilityOfPassingRuleFormatted, err := readGeneticAnalysis.GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) + offspringRuleProbabilityKnown, _, offspringProbabilityOfPassingRuleFormatted, err := readGeneticAnalysis.GetOffspringDiscreteTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) if (err != nil) { return err } getOffspringProbabilityOfPassingRuleText := func()string{ @@ -2608,11 +2896,11 @@ func setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, person return offspringProbabilityOfPassingRuleFormatted } - + offspringProbabilityOfPassingRuleText := getOffspringProbabilityOfPassingRuleText() viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) + setViewCoupleGeneticAnalysisDiscreteTraitGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, traitName, genomePairIdentifier, genomePairName, currentPage) }) genomePairNameLabel := getBoldLabelCentered(genomePairName) diff --git a/gui/viewAnalysisGui_Person.go b/gui/viewAnalysisGui_Person.go index b2b6eec..d3f3fc5 100644 --- a/gui/viewAnalysisGui_Person.go +++ b/gui/viewAnalysisGui_Person.go @@ -13,6 +13,7 @@ import "fyne.io/fyne/v2/layout" import "fyne.io/fyne/v2/theme" import "fyne.io/fyne/v2/widget" +import "seekia/resources/geneticPredictionModels" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" @@ -70,11 +71,15 @@ func setViewPersonGeneticAnalysisPage(window fyne.Window, personIdentifier strin polygenicDiseasesButton := widget.NewButton("Polygenic Diseases", func(){ setViewPersonGeneticAnalysisPolygenicDiseasesPage(window, personIdentifier, analysisObject, currentPage) }) - traitsButton := widget.NewButton("Traits", func(){ - setViewPersonGeneticAnalysisTraitsPage(window, personIdentifier, analysisObject, currentPage) + discreteTraitsButton := widget.NewButton("Discrete Traits", func(){ + setViewPersonGeneticAnalysisDiscreteTraitsPage(window, personIdentifier, analysisObject, currentPage) + }) + numericTraitsButton := widget.NewButton("Numeric Traits", func(){ + //TODO + showUnderConstructionDialog(window) }) - categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, traitsButton)) + categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, discreteTraitsButton, numericTraitsButton)) page := container.NewVBox(title, backButton, widget.NewSeparator(), warningLabel1, warningLabel2, widget.NewSeparator(), personNameRow, numberOfAnalyzedGenomesRow, widget.NewSeparator(), categoryButtonsGrid) @@ -94,7 +99,7 @@ func setViewPersonGeneticAnalysisMonogenicDiseasesPage(window fyne.Window, analy getMonogenicDiseasesContainer := func()(*fyne.Container, error){ - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) if (err != nil){ return nil, err } // Outputs: @@ -240,8 +245,8 @@ func setViewPersonGeneticAnalysisMonogenicDiseaseDetailsPage(window fyne.Window, title := getPageTitleCentered("Viewing Genetic Analysis - " + diseaseName) backButton := getBackButtonCentered(previousPage) - - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return @@ -280,8 +285,6 @@ func setViewPersonGeneticAnalysisMonogenicDiseaseDetailsPage(window fyne.Window, totalNumberOfVariants := len(diseaseVariantsMap) totalNumberOfVariantsString := helpers.ConvertIntToString(totalNumberOfVariants) - - emptyLabelA := widget.NewLabel("") genomeNameLabel := getItalicLabelCentered("Genome Name") @@ -736,7 +739,7 @@ func setViewPersonGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window fyne. getGenomesHaveVariantGrid := func()(*fyne.Container, error){ - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisObject) if (err != nil) { return nil, err } genomeNameLabel := getItalicLabelCentered("Genome Name") @@ -894,7 +897,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseasesPage(window fyne.Window, perso getPolygenicDiseasesContainer := func()(*fyne.Container, error){ - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) if (err != nil){ return nil, err } // Outputs: @@ -935,7 +938,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseasesPage(window fyne.Window, perso diseaseNameText := getBoldLabelCentered(diseaseName) - personRiskScoreKnown, _, personRiskScoreFormatted, _, _, conflictExists, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, mainGenomeIdentifier) + personRiskScoreKnown, _, personRiskScoreFormatted, _, conflictExists, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, mainGenomeIdentifier) if (err != nil) { return nil, err } getPersonRiskScoreLabelText := func()string{ @@ -1015,7 +1018,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, backButton := getBackButtonCentered(previousPage) - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return @@ -1054,7 +1057,6 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, totalNumberOfLoci := len(diseaseLociMap) totalNumberOfLociString := helpers.ConvertIntToString(totalNumberOfLoci) - emptyLabelA := widget.NewLabel("") genomeNameLabel := getItalicLabelCentered("Genome Name") @@ -1087,7 +1089,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setCombinedGenomesExplainerPage(window, currentPage) }) - + genomeNameLabel := getBoldLabel(genomeName) genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) @@ -1096,7 +1098,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, genomeNameCell := getGenomeNameCell() - diseaseRiskScoreKnown, _, diseaseRiskScoreFormatted, _, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, genomeIdentifier) + diseaseRiskScoreKnown, _, diseaseRiskScoreFormatted, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, genomeIdentifier) if (err != nil) { return err } getRiskScoreLabelText := func()string{ @@ -1622,7 +1624,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Wi getGenomesLocusInfoGrid := func()(*fyne.Container, error){ - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisObject) if (err != nil) { return nil, err } genomeNameLabel := getItalicLabelCentered("Genome Name") @@ -1765,19 +1767,19 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Wi } -func setViewPersonGeneticAnalysisTraitsPage(window fyne.Window, personIdentifier string, analysisObject geneticAnalysis.PersonAnalysis, previousPage func()){ +func setViewPersonGeneticAnalysisDiscreteTraitsPage(window fyne.Window, personIdentifier string, analysisObject geneticAnalysis.PersonAnalysis, previousPage func()){ - currentPage := func(){setViewPersonGeneticAnalysisTraitsPage(window, personIdentifier, analysisObject, previousPage)} + currentPage := func(){setViewPersonGeneticAnalysisDiscreteTraitsPage(window, personIdentifier, analysisObject, previousPage)} - title := getPageTitleCentered("Viewing Genetic Analysis - Traits") + title := getPageTitleCentered("Viewing Genetic Analysis - Discrete Traits") backButton := getBackButtonCentered(previousPage) - description := getLabelCentered("Below is an analysis of the traits for this person's genome.") + description := getLabelCentered("Below is an analysis of the discrete traits for this person's genome.") getTraitsContainer := func()(*fyne.Container, error){ - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) if (err != nil){ return nil, err } // Outputs: @@ -1798,102 +1800,154 @@ func setViewPersonGeneticAnalysisTraitsPage(window fyne.Window, personIdentifier mainGenomeIdentifier, err := getMainGenomeIdentifier() if (err != nil){ return nil, err } + emptyLabel1 := widget.NewLabel("") traitNameLabel := getItalicLabelCentered("Trait Name") - outcomeScoresLabel := getItalicLabelCentered("Outcome Scores") + emptyLabel2 := widget.NewLabel("") + predictedOutcomeLabel := getItalicLabelCentered("Predicted Outcome") + quantityOfLabel := getItalicLabelCentered("Quantity Of") + lociKnownLabel := getItalicLabelCentered("Loci Known") + + emptyLabel3 := widget.NewLabel("") conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") - emptyLabel := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") - traitNameColumn := container.NewVBox(traitNameLabel, widget.NewSeparator()) - outcomeScoresColumn := container.NewVBox(outcomeScoresLabel, widget.NewSeparator()) - conflictExistsColumn := container.NewVBox(conflictExistsLabel, widget.NewSeparator()) - viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + traitNameColumn := container.NewVBox(emptyLabel1, traitNameLabel, widget.NewSeparator()) + predictedOutcomeColumn := container.NewVBox(emptyLabel2, predictedOutcomeLabel, widget.NewSeparator()) + quantityOfLociKnownColumn := container.NewVBox(quantityOfLabel, lociKnownLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(emptyLabel3, conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel4, emptyLabel5, widget.NewSeparator()) traitObjectsList, err := traits.GetTraitObjectsList() if (err != nil) { return nil, err } for _, traitObject := range traitObjectsList{ - traitName := traitObject.TraitName - traitRulesList := traitObject.RulesList - - if (len(traitRulesList) == 0){ - // This trait does not have any rules - // We cannot analyze it yet - // We will add neural network prediction so we can predict these traits + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ continue } - traitOutcomeNamesList := traitObject.OutcomesList - _, anyTraitRuleTested, outcomeScoresMap, _, conflictExists, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(analysisObject, traitName, mainGenomeIdentifier) + traitName := traitObject.TraitName + traitLociList := traitObject.LociList + traitRulesList := traitObject.RulesList + + if (len(traitLociList) == 0 && len(traitRulesList) == 0){ + // This trait does not have any rules or loci to analyze + // We cannot analyze it yet + continue + } + + traitNameText := getBoldLabelCentered(traitName) + + neuralNetworkExists, neuralNetworkAnalysisExists, neuralNetworkPredictedOutcome, _, quantityOfLociKnown_NeuralNetwork, _, anyRulesExist, rulesAnalysisExists, _, rulesPredictedOutcomeExists, rulesPredictedOutcome, _, quantityOfLociKnown_Rules, conflictExists, err := readGeneticAnalysis.GetPersonDiscreteTraitInfoFromGeneticAnalysis(analysisObject, traitName, mainGenomeIdentifier) if (err != nil) { return nil, err } + if (neuralNetworkExists == false && anyRulesExist == false){ + // We can't analyze this trait + continue + } - // We add all of the columns except for the trait outcomes column, which may be multiple rows high + getPredictionLabel := func()fyne.Widget{ + + if (neuralNetworkExists == true){ + + if (neuralNetworkAnalysisExists == false){ + result := getItalicLabel("Unknown") + return result + } + + result := getBoldLabel(neuralNetworkPredictedOutcome) + + return result + } + // anyRulesExist must be true + + if (rulesAnalysisExists == false || rulesPredictedOutcomeExists == false){ + result := getItalicLabel("Unknown") + return result + } + + result := getBoldLabel(rulesPredictedOutcome) + + return result + } + + predictionLabel := getPredictionLabel() + + predictionLabelCentered := getWidgetCentered(predictionLabel) + + getQuantityOfLociKnown := func()int{ + + if (neuralNetworkExists == true){ + + return quantityOfLociKnown_NeuralNetwork + } + return quantityOfLociKnown_Rules + } + + quantityOfLociKnown := getQuantityOfLociKnown() + + getTotalQuantityOfLoci := func()int{ + + if (neuralNetworkExists == true){ + + totalQuantityOfLoci := len(traitLociList) + + return totalQuantityOfLoci + } + + traitLociList_Rules := traitObject.LociList_Rules + + totalQuantityOfLoci := len(traitLociList_Rules) + + return totalQuantityOfLoci + } + + totalQuantityOfLoci := getTotalQuantityOfLoci() + + 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(){ - setViewPersonGeneticAnalysisTraitDetailsPage(window, personIdentifier, analysisObject, traitName, currentPage) + setViewPersonGeneticAnalysisDiscreteTraitDetailsPage(window, personIdentifier, analysisObject, traitName, currentPage) })) - traitNameText := getBoldLabelCentered(traitName) - traitNameColumn.Add(traitNameText) + predictedOutcomeColumn.Add(predictionLabelCentered) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) conflictExistsColumn.Add(conflictExistsLabel) viewButtonsColumn.Add(viewDetailsButton) - if (anyTraitRuleTested == false){ - - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) - - outcomeScoresColumn.Add(unknownLabel) - } else { - - // We have to sort outcome names so they always show up in the same order - - traitOutcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(traitOutcomeNamesList) - - for index, outcomeName := range traitOutcomeNamesListSorted{ - - outcomeScore, exists := outcomeScoresMap[outcomeName] - if (exists == false){ - return nil, errors.New("Outcome name not found in outcomeScoresMap: " + outcomeName) - } - - outcomeScoreString := helpers.ConvertIntToString(outcomeScore) - - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - outcomeScoresColumn.Add(outcomeRow) - - if (index > 0){ - - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") - - traitNameColumn.Add(emptyLabelA) - conflictExistsColumn.Add(emptyLabelB) - viewButtonsColumn.Add(emptyLabelC) - } - } - } - traitNameColumn.Add(widget.NewSeparator()) - outcomeScoresColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) conflictExistsColumn.Add(widget.NewSeparator()) viewButtonsColumn.Add(widget.NewSeparator()) } - outcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitOutcomeScoresExplainerPage(window, currentPage) + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) }) - outcomeScoresColumn.Add(outcomeScoresHelpButton) + predictedOutcomeColumn.Add(predictedOutcomeHelpButton) - traitsContainer := container.NewHBox(layout.NewSpacer(), traitNameColumn, outcomeScoresColumn) + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) + + traitsContainer := container.NewHBox(layout.NewSpacer(), traitNameColumn, predictedOutcomeColumn, quantityOfLociKnownColumn) if (multipleGenomesExist == true){ @@ -1923,16 +1977,15 @@ func setViewPersonGeneticAnalysisTraitsPage(window fyne.Window, personIdentifier } +func setViewPersonGeneticAnalysisDiscreteTraitDetailsPage(window fyne.Window, personIdentifier string, analysisObject geneticAnalysis.PersonAnalysis, traitName string, previousPage func()){ -func setViewPersonGeneticAnalysisTraitDetailsPage(window fyne.Window, personIdentifier string, analysisObject geneticAnalysis.PersonAnalysis, traitName string, previousPage func()){ - - currentPage := func(){setViewPersonGeneticAnalysisTraitDetailsPage(window, personIdentifier, analysisObject, traitName, previousPage)} + currentPage := func(){setViewPersonGeneticAnalysisDiscreteTraitDetailsPage(window, personIdentifier, analysisObject, traitName, previousPage)} title := getPageTitleCentered("Viewing Genetic Analysis - " + traitName) backButton := getBackButtonCentered(previousPage) - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisObject) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return @@ -1961,33 +2014,51 @@ func setViewPersonGeneticAnalysisTraitDetailsPage(window fyne.Window, personIden traitInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ setViewTraitDetailsPage(window, traitName, currentPage) }) - traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitInfoButton, layout.NewSpacer()) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitInfoButton, layout.NewSpacer()) + + neuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) getGenomesContainer := func()(*fyne.Container, error){ - traitRulesMap, err := traits.GetTraitRulesMap(traitName) - if (err != nil){ return nil, err } - - totalNumberOfRules := len(traitRulesMap) - totalNumberOfRulesString := helpers.ConvertIntToString(totalNumberOfRules) - - emptyLabelA := widget.NewLabel("") + emptyLabel1 := widget.NewLabel("") genomeNameLabel := getItalicLabelCentered("Genome Name") - - emptyLabelB := widget.NewLabel("") - outcomeScoresLabel := getItalicLabelCentered("Outcome Scores") - - numberOfLabel := getItalicLabelCentered("Number of") + + emptyLabel2 := widget.NewLabel("") + predictedOutcomeLabel := getItalicLabelCentered("Predicted Outcome") + + emptyLabel3 := widget.NewLabel("") + predictionConfidenceLabel := getItalicLabelCentered("Prediction Confidence") + + quantityOfLabel := getItalicLabelCentered("Quantity Of") rulesTestedLabel := getItalicLabelCentered("Rules Tested") - emptyLabelD := widget.NewLabel("") - emptyLabelE := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") + + genomeNameColumn := container.NewVBox() + predictedOutcomeColumn := container.NewVBox() + neuralNetworkPredictionConfidenceColumn := container.NewVBox() + numberOfRulesTestedColumn := container.NewVBox(quantityOfLabel, rulesTestedLabel, widget.NewSeparator()) + viewDetailsButtonsColumn := container.NewVBox() + + if (neuralNetworkExists == false){ + // We only need an extra header row if the numberOfRulesTested column is shown + genomeNameColumn.Add(emptyLabel1) + predictedOutcomeColumn.Add(emptyLabel2) + neuralNetworkPredictionConfidenceColumn.Add(emptyLabel3) + viewDetailsButtonsColumn.Add(emptyLabel4) + } + + genomeNameColumn.Add(genomeNameLabel) + predictedOutcomeColumn.Add(predictedOutcomeLabel) + neuralNetworkPredictionConfidenceColumn.Add(predictionConfidenceLabel) + viewDetailsButtonsColumn.Add(emptyLabel5) + + genomeNameColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + neuralNetworkPredictionConfidenceColumn.Add(widget.NewSeparator()) + viewDetailsButtonsColumn.Add(widget.NewSeparator()) - genomeNameColumn := container.NewVBox(emptyLabelA, genomeNameLabel, widget.NewSeparator()) - outcomeScoresColumn := container.NewVBox(emptyLabelB, outcomeScoresLabel, widget.NewSeparator()) - numberOfRulesTestedColumn := container.NewVBox(numberOfLabel, rulesTestedLabel, widget.NewSeparator()) - viewRulesButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) - addGenomeRow := func(genomeName string, genomeIdentifier [16]byte, isACombinedGenome bool)error{ getGenomeNameCell := func()*fyne.Container{ @@ -2008,66 +2079,121 @@ func setViewPersonGeneticAnalysisTraitDetailsPage(window fyne.Window, personIden genomeNameCell := getGenomeNameCell() - _, anyTraitRuleTested, outcomeScoresMap, numberOfRulesTested, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(analysisObject, traitName, genomeIdentifier) + neuralNetworkExists, neuralNetworkAnalysisExists, neuralNetworkPredictedOutcome, neuralNetworkPredictionConfidence, _, _, anyRulesExist, rulesAnalysisExists, _, rulesPredictedOutcomeExists, rulesPredictedOutcome, quantityOfRulesTested, _, _, err := readGeneticAnalysis.GetPersonDiscreteTraitInfoFromGeneticAnalysis(analysisObject, traitName, genomeIdentifier) if (err != nil) { return err } - // We add all of the columns except for the trait rule column, which may be multiple rows high + if (neuralNetworkExists == false && anyRulesExist == false){ + // This trait is not analyzable + return errors.New("setViewPersonGeneticAnalysisTraitDetailsPage called with trait which cannot be analyzed via neural networks and traits.") + } - genomeNumberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) - numberOfRulesTestedLabel := getBoldLabelCentered(genomeNumberOfRulesTestedString + "/" + totalNumberOfRulesString) + getPredictedOutcomeLabel := func()fyne.Widget{ - viewRulesButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewPersonGenomeTraitRulesPage(window, analysisObject, traitName, genomeIdentifier, genomeName, currentPage) + if (neuralNetworkExists == true){ + if (neuralNetworkAnalysisExists == false){ + + predictedOutcomeLabel := getItalicLabel("Unknown") + + return predictedOutcomeLabel + } + + predictedOutcomeLabel := getBoldLabel(neuralNetworkPredictedOutcome) + + return predictedOutcomeLabel + } + + // anyRulesExist must be true + + if (rulesAnalysisExists == false || rulesPredictedOutcomeExists == false){ + + predictedOutcomeLabel := getItalicLabel("Unknown") + return predictedOutcomeLabel + } + + predictedOutcomeLabel := getBoldLabel(rulesPredictedOutcome) + + return predictedOutcomeLabel + } + + predictedOutcomeLabel := getPredictedOutcomeLabel() + + predictedOutcomeLabelCentered := getWidgetCentered(predictedOutcomeLabel) + + getNeuralNetworkConfidenceLabel := func()fyne.Widget{ + + if (neuralNetworkExists == false){ + + emptyLabel := widget.NewLabel("") + + return emptyLabel + } + if (neuralNetworkAnalysisExists == false){ + + predictedOutcomeConfidenceLabel := getItalicLabel("Unknown") + + return predictedOutcomeConfidenceLabel + } + + neuralNetworkPredictionConfidenceString := helpers.ConvertIntToString(neuralNetworkPredictionConfidence) + predictedOutcomeConfidenceLabel := getBoldLabel(neuralNetworkPredictionConfidenceString + "%") + + return predictedOutcomeConfidenceLabel + } + + predictedOutcomeConfidenceLabel := getNeuralNetworkConfidenceLabel() + predictedOutcomeConfidenceLabelCentered := getWidgetCentered(predictedOutcomeConfidenceLabel) + + getQuantityOfRulesTestedLabel := func()(fyne.Widget, error){ + + if (anyRulesExist == false){ + + emptyLabel := widget.NewLabel("") + return emptyLabel, nil + } + + traitRulesMap, err := traits.GetTraitRulesMap(traitName) + if (err != nil){ return nil, err } + + totalNumberOfRules := len(traitRulesMap) + totalNumberOfRulesString := helpers.ConvertIntToString(totalNumberOfRules) + + if (rulesAnalysisExists == false){ + quantityOfRulesTestedLabel := getItalicLabel("0/" + totalNumberOfRulesString) + return quantityOfRulesTestedLabel, nil + } + + quantityOfRulesTestedString := helpers.ConvertIntToString(quantityOfRulesTested) + + quantityOfRulesTestedLabel := getBoldLabel(quantityOfRulesTestedString + "/" + totalNumberOfRulesString) + + return quantityOfRulesTestedLabel, nil + } + + quantityOfRulesTestedLabel, err := getQuantityOfRulesTestedLabel() + if (err != nil) { return err } + + quantityOfRulesTestedLabelCentered := getWidgetCentered(quantityOfRulesTestedLabel) + + viewDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + if (neuralNetworkExists == true){ + //TODO + showUnderConstructionDialog(window) + } else { + setViewPersonGenomeDiscreteTraitRulesPage(window, analysisObject, traitName, genomeIdentifier, genomeName, currentPage) + } }) genomeNameColumn.Add(genomeNameCell) - numberOfRulesTestedColumn.Add(numberOfRulesTestedLabel) - viewRulesButtonsColumn.Add(viewRulesButton) + predictedOutcomeColumn.Add(predictedOutcomeLabelCentered) + neuralNetworkPredictionConfidenceColumn.Add(predictedOutcomeConfidenceLabelCentered) + numberOfRulesTestedColumn.Add(quantityOfRulesTestedLabelCentered) + viewDetailsButtonsColumn.Add(viewDetailsButton) - if (anyTraitRuleTested == false){ - - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) - - outcomeScoresColumn.Add(unknownLabel) - - } else { - - // We have to sort the outcome names so they always show up in the same order - - outcomeNamesList := helpers.GetListOfMapKeys(outcomeScoresMap) - - helpers.SortStringListToUnicodeOrder(outcomeNamesList) - - for index, outcomeName := range outcomeNamesList{ - - outcomeScore, exists := outcomeScoresMap[outcomeName] - if (exists == false){ - return errors.New("Outcome name not found in outcome scores map after being found already: " + outcomeName) - } - - outcomeScoreString := helpers.ConvertIntToString(outcomeScore) - - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - outcomeScoresColumn.Add(outcomeRow) - - if (index > 0){ - - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") - - genomeNameColumn.Add(emptyLabelA) - numberOfRulesTestedColumn.Add(emptyLabelB) - viewRulesButtonsColumn.Add(emptyLabelC) - } - } - } - genomeNameColumn.Add(widget.NewSeparator()) - outcomeScoresColumn.Add(widget.NewSeparator()) + predictedOutcomeColumn.Add(widget.NewSeparator()) + neuralNetworkPredictionConfidenceColumn.Add(widget.NewSeparator()) numberOfRulesTestedColumn.Add(widget.NewSeparator()) - viewRulesButtonsColumn.Add(widget.NewSeparator()) + viewDetailsButtonsColumn.Add(widget.NewSeparator()) return nil } @@ -2112,17 +2238,41 @@ func setViewPersonGeneticAnalysisTraitDetailsPage(window fyne.Window, personIden if (err != nil){ return nil, err } } - outcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitOutcomeScoresExplainerPage(window, currentPage) + if (neuralNetworkExists == true){ + + neuralNetworksHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setDiscreteTraitNeuralNetworkPredictionExplainerPage(window, currentPage) + }) + predictedOutcomeColumn.Add(neuralNetworksHelpButton) + } else { + + rulesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setDiscreteTraitRulesPredictionExplainerPage(window, currentPage) + }) + predictedOutcomeColumn.Add(rulesHelpButton) + } + + neuralNetworkConfidenceHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + showUnderConstructionDialog(window) + //TODO }) - outcomeScoresColumn.Add(outcomeScoresHelpButton) + neuralNetworkPredictionConfidenceColumn.Add(neuralNetworkConfidenceHelpButton) numberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + setDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) }) numberOfRulesTestedColumn.Add(numberOfRulesTestedHelpButton) - genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, outcomeScoresColumn, numberOfRulesTestedColumn, viewRulesButtonsColumn, layout.NewSpacer()) + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, predictedOutcomeColumn) + + if (neuralNetworkExists == true){ + genomesContainer.Add(neuralNetworkPredictionConfidenceColumn) + } else { + genomesContainer.Add(numberOfRulesTestedColumn) + } + + genomesContainer.Add(viewDetailsButtonsColumn) + genomesContainer.Add(layout.NewSpacer()) return genomesContainer, nil } @@ -2140,12 +2290,12 @@ func setViewPersonGeneticAnalysisTraitDetailsPage(window fyne.Window, personIden -// Ths function provides a page to view the trait rules for a particular genome from a genetic analysis -func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, genomeIdentifier [16]byte, genomeName string, previousPage func()){ +// Ths function provides a page to view the discrete trait rules for a particular genome from a genetic analysis +func setViewPersonGenomeDiscreteTraitRulesPage(window fyne.Window, geneticAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, genomeIdentifier [16]byte, genomeName string, previousPage func()){ setLoadingScreen(window, "Loading Trait Rules", "Loading trait rules...") - currentPage := func(){setViewPersonGenomeTraitRulesPage(window, geneticAnalysisObject, traitName, genomeIdentifier, genomeName, previousPage)} + currentPage := func(){setViewPersonGenomeDiscreteTraitRulesPage(window, geneticAnalysisObject, traitName, genomeIdentifier, genomeName, previousPage)} title := getPageTitleCentered("View Trait Rules - " + traitName) @@ -2153,7 +2303,7 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject description1 := widget.NewLabel("Below are the trait rule results for this genome.") rulesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitRulesExplainerPage(window, currentPage) + setDiscreteTraitRulesExplainerPage(window, currentPage) }) description1Row := container.NewHBox(layout.NewSpacer(), description1, rulesHelpButton, layout.NewSpacer()) @@ -2195,7 +2345,7 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject return } - ruleStatusIsKnown, genomePassesRule, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(geneticAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) + ruleStatusIsKnown, genomePassesRule, err := readGeneticAnalysis.GetPersonDiscreteTraitRuleInfoFromGeneticAnalysis(geneticAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return @@ -2221,7 +2371,7 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject rulesTestedLabel := widget.NewLabel("Rules Tested:") rulesTestedText := getBoldLabel(numberOfRulesTestedString + "/" + totalNumberOfRulesString) rulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + setDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) }) rulesTestedRow := container.NewHBox(layout.NewSpacer(), rulesTestedLabel, rulesTestedText, rulesTestedHelpButton, layout.NewSpacer()) @@ -2245,7 +2395,7 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) if (err != nil){ return err } - ruleStatusIsKnown, genomePassesRule, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(geneticAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) + ruleStatusIsKnown, genomePassesRule, err := readGeneticAnalysis.GetPersonDiscreteTraitRuleInfoFromGeneticAnalysis(geneticAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) if (err != nil) { return err } getGenomePassesRuleText := func()string{ @@ -2267,7 +2417,7 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject // We do this because the rule effects column may be multiple rows tall viewRuleButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewPersonGeneticAnalysisTraitRuleDetailsPage(window, geneticAnalysisObject, traitName, ruleIdentifier, currentPage) + setViewPersonGeneticAnalysisDiscreteTraitRuleDetailsPage(window, geneticAnalysisObject, traitName, ruleIdentifier, currentPage) }) ruleIdentifierColumn.Add(ruleIdentifierLabel) @@ -2352,12 +2502,12 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject } ruleEffectsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + setDiscreteTraitRuleOutcomeEffectsExplainerPage(window, currentPage) }) ruleEffectsColumn.Add(ruleEffectsHelpButton) genomePassesRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setGenomePassesTraitRuleExplainerPage(window, currentPage) + setGenomePassesDiscreteTraitRuleExplainerPage(window, currentPage) }) genomePassesRuleColumn.Add(genomePassesRuleHelpButton) @@ -2381,9 +2531,9 @@ func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisObject // This function provides a page to view the details of a specific trait rule from a person genetic analysis // The page will show the rule details for all of the person's genomes -func setViewPersonGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, geneticAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, ruleIdentifier [3]byte, previousPage func()){ +func setViewPersonGeneticAnalysisDiscreteTraitRuleDetailsPage(window fyne.Window, geneticAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, ruleIdentifier [3]byte, previousPage func()){ - currentPage := func(){setViewPersonGeneticAnalysisTraitRuleDetailsPage(window, geneticAnalysisObject, traitName, ruleIdentifier, previousPage)} + currentPage := func(){setViewPersonGeneticAnalysisDiscreteTraitRuleDetailsPage(window, geneticAnalysisObject, traitName, ruleIdentifier, previousPage)} title := getPageTitleCentered("Trait Rule Details - " + traitName) @@ -2396,13 +2546,13 @@ func setViewPersonGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, geneti ruleIdentifierLabel := widget.NewLabel("Rule Identifier:") ruleIdentifierText := getBoldLabel(ruleIdentifierHex) ruleInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewTraitRuleDetailsPage(window, traitName, ruleIdentifierHex, currentPage) + setViewDiscreteTraitRuleDetailsPage(window, traitName, ruleIdentifierHex, currentPage) }) ruleIdentifierRow := container.NewHBox(layout.NewSpacer(), ruleIdentifierLabel, ruleIdentifierText, ruleInfoButton, layout.NewSpacer()) getGenomesRuleInfoGrid := func()(*fyne.Container, error){ - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisObject) if (err != nil) { return nil, err } genomeNameLabel := getItalicLabelCentered("Genome Name") @@ -2413,7 +2563,7 @@ func setViewPersonGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, geneti addGenomeRow := func(genomeName string, genomeIdentifier [16]byte, isACombinedGenome bool)error{ - genomeRuleStatusKnown, genomePassesRule, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(geneticAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) + genomeRuleStatusKnown, genomePassesRule, err := readGeneticAnalysis.GetPersonDiscreteTraitRuleInfoFromGeneticAnalysis(geneticAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) if (err != nil) { return err } getGenomePassesRuleText := func()string{ @@ -2501,7 +2651,7 @@ func setViewPersonGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, geneti } genomePassesRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setGenomePassesTraitRuleExplainerPage(window, currentPage) + setGenomePassesDiscreteTraitRuleExplainerPage(window, currentPage) }) genomePassesRuleColumn.Add(genomePassesRuleHelpButton) diff --git a/gui/viewGeneticReferencesGui.go b/gui/viewGeneticReferencesGui.go index b585ac5..94397eb 100644 --- a/gui/viewGeneticReferencesGui.go +++ b/gui/viewGeneticReferencesGui.go @@ -510,7 +510,7 @@ func setViewTraitDetailsPage(window fyne.Window, traitName string, previousPage } traitDescription := traitObject.TraitDescription - traitReferencesMap := traitObject.References + traitReferencesMap := traitObject.ReferencesMap traitNameLabel := widget.NewLabel("Trait Name:") traitNameText := getBoldLabel(traitName) @@ -538,9 +538,9 @@ func setViewTraitDetailsPage(window fyne.Window, traitName string, previousPage setPageContent(page, window) } -func setViewTraitRuleDetailsPage(window fyne.Window, traitName string, ruleIdentifier string, previousPage func()){ +func setViewDiscreteTraitRuleDetailsPage(window fyne.Window, traitName string, ruleIdentifier string, previousPage func()){ - currentPage := func(){setViewTraitRuleDetailsPage(window, traitName, ruleIdentifier, previousPage)} + currentPage := func(){setViewDiscreteTraitRuleDetailsPage(window, traitName, ruleIdentifier, previousPage)} title := getPageTitleCentered("Viewing Rule Details") @@ -561,7 +561,7 @@ func setViewTraitRuleDetailsPage(window fyne.Window, traitName string, ruleIdent } ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap - ruleReferencesMap := traitRuleObject.References + ruleReferencesMap := traitRuleObject.ReferencesMap viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){ setViewGeneticAnalysisReferencesPage(window, "Rule", ruleReferencesMap, currentPage) @@ -605,7 +605,7 @@ func setViewTraitRuleDetailsPage(window fyne.Window, traitName string, ruleIdent } outcomeEffectHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + setDiscreteTraitRuleOutcomeEffectsExplainerPage(window, currentPage) }) outcomeEffectColumn.Add(outcomeEffectHelpButton) diff --git a/gui/viewProfileGui.go b/gui/viewProfileGui.go index c85121a..12b53a0 100644 --- a/gui/viewProfileGui.go +++ b/gui/viewProfileGui.go @@ -12,6 +12,7 @@ import "fyne.io/fyne/v2/widget" import "seekia/resources/worldLanguages" import "seekia/resources/worldLocations" +import "seekia/resources/geneticPredictionModels" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" @@ -1270,7 +1271,7 @@ func setViewUserProfilePage_Category(window fyne.Window, profileIsMine bool, cat }) geneticTraitsButton := widget.NewButton("Genetic Traits", func(){ - setViewMateProfilePage_GeneticTraits(window, "Offspring", getAnyUserProfileAttributeFunction, currentPage) + setViewMateProfilePage_GeneticTraits(window, getAnyUserProfileAttributeFunction, currentPage) }) buttonsGrid := getContainerCentered(container.NewGridWithColumns(2, racialSimilarityButton, totalDiseaseRiskButton, ancestryCompositionButton, monogenicDiseasesButton, haplogroupsButton, polygenicDiseasesButton, neanderthalVariantsButton, geneticTraitsButton)) @@ -2097,9 +2098,9 @@ func setViewMateProfilePage_RacialSimilarity(window fyne.Window, getAnyUserProfi if (err != nil) { return err } traitLociList := traitObject.LociList - numberOfTraitLoci := len(traitLociList) + quantityOfTraitLoci := len(traitLociList) - numberOfTraitLociString := helpers.ConvertIntToString(numberOfTraitLoci) + quantityOfTraitLociString := helpers.ConvertIntToString(quantityOfTraitLoci) geneticSimilarityIsKnown, _, attributeValue, err := getAnyUserProfileAttributeFunction(geneticSimilarityAttributeName) if (err != nil) { return err } @@ -2108,11 +2109,11 @@ func setViewMateProfilePage_RacialSimilarity(window fyne.Window, getAnyUserProfi geneticSimilarityColumn.Add(unknownLabel) - numberOfTestedLociText := "0/" + numberOfTraitLociString + quantityOfTestedLociText := "0/" + quantityOfTraitLociString - numberOfTestedLociLabel := getBoldLabelCentered(numberOfTestedLociText) + quantityOfTestedLociLabel := getBoldLabelCentered(quantityOfTestedLociText) - numberOfTestedLociColumn.Add(numberOfTestedLociLabel) + numberOfTestedLociColumn.Add(quantityOfTestedLociLabel) } else { similarityFormatted := attributeValue + "%" @@ -2147,7 +2148,7 @@ func setViewMateProfilePage_RacialSimilarity(window fyne.Window, getAnyUserProfi numberOfTestedLociString := helpers.ConvertIntToString(numberOfTestedLoci) - numberOfTestedLociLabelText := numberOfTestedLociString + "/" + numberOfTraitLociString + numberOfTestedLociLabelText := numberOfTestedLociString + "/" + quantityOfTraitLociString numberOfTestedLociLabel := getBoldLabelCentered(numberOfTestedLociLabelText) @@ -2959,24 +2960,25 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin //Outputs: // -map[int64]locusValue.LocusValue // -error - getMyDiseaseLocusValuesMap := func()(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 } - anyMyLociValuesExist, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, myGenomeIdentifier) + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myAnalysisObject) if (err != nil) { return nil, err } - if (anyMyLociValuesExist == false){ - emptyMap := make(map[int64]locusValue.LocusValue) - return emptyMap, nil + + myGenomeLocusValuesMap, exists := myGenomesMap[myGenomeIdentifier] + if (exists == false){ + return nil, errors.New("GetMyChosenMateGeneticAnalysis returning analysis which is missing genome with myGenomeIdentifier.") } - return myDiseaseLocusValuesMap, nil + return myGenomeLocusValuesMap, nil } - myDiseaseLocusValuesMap, err := getMyDiseaseLocusValuesMap() + myGenomeLocusValuesMap, err := getMyGenomeLocusValuesMap() if (err != nil) { return nil, err } // Map Structure: Locus rsID -> Locus Value @@ -3031,7 +3033,7 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin userDiseaseRiskScoreString, err := getUserDiseaseRiskScoreString() if (err != nil) { return nil, err } - anyOffspringLociTested, offspringDiseaseRiskScore, offspringNumberOfLociTested, _, offspringSampleRiskScoresList, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo(diseaseLociList, myDiseaseLocusValuesMap, userDiseaseLocusValuesMap) + anyOffspringLociTested, offspringDiseaseRiskScore, offspringNumberOfLociTested, _, offspringSampleRiskScoresList, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo(diseaseLociList, myGenomeLocusValuesMap, userDiseaseLocusValuesMap) if (err != nil) { return nil, err } getOffspringDiseaseRiskScoreFormatted := func()(string, error){ @@ -3172,7 +3174,7 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName // Outputs: // -map[int64]locusValue.LocusValue: Map Structure: Locus rsID -> Locus Value // -error - getMyDiseaseLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ + getMyGenomeLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() if (err != nil) { return nil, err } @@ -3183,17 +3185,18 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName return emptyMap, nil } - anyLocusValuesExist, _, _, myLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, myGenomeIdentifier) + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myAnalysisObject) if (err != nil) { return nil, err } - if (anyLocusValuesExist == false){ - emptyMap := make(map[int64]locusValue.LocusValue) - return emptyMap, nil + + myGenomeLocusValuesMap, exists := myGenomesMap[myGenomeIdentifier] + if (exists == false){ + return nil, errors.New("GetMyChosenMateGeneticAnalysis returning analysis which is missing genome with myGenomeIdentifier.") } - return myLocusValuesMap, nil + return myGenomeLocusValuesMap, nil } - myDiseaseLocusValuesMap, err := getMyDiseaseLocusValuesMap() + myGenomeLocusValuesMap, err := getMyGenomeLocusValuesMap() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return @@ -3248,7 +3251,7 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName return } - anyOffspringLociTested, _, offspringNumberOfLociTested, offspringLociInfoMap, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo(diseaseLocusObjectsList, myDiseaseLocusValuesMap, userDiseaseLocusValuesMap) + anyOffspringLociTested, _, offspringNumberOfLociTested, offspringLociInfoMap, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo(diseaseLocusObjectsList, myGenomeLocusValuesMap, userDiseaseLocusValuesMap) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return @@ -3513,9 +3516,9 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName setPageContent(page, window) } -func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ +func setViewMateProfilePage_GeneticTraits(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ - currentPage := func(){setViewMateProfilePage_GeneticTraits(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + currentPage := func(){setViewMateProfilePage_GeneticTraits(window, getAnyUserProfileAttributeFunction, previousPage)} title := getPageTitleCentered(translate("View Profile - Physical")) @@ -3523,7 +3526,46 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st subtitle := getPageSubtitleCentered(translate("Genetic Traits")) - description1 := getLabelCentered("Below is the genetic trait analysis for this user.") + description1 := getLabelCentered("Choose if you want to view Discrete traits or Numeric traits.") + description2 := getLabelCentered("Discrete traits are traits which have discrete outcomes, such as Eye Color") + description3 := getLabelCentered("Numeric traits are traits with numeric outcomes, such as Height.") + + discreteTraitsButton := widget.NewButton("Discrete Traits", func(){ + setViewMateProfilePage_DiscreteGeneticTraits(window, "Offspring", getAnyUserProfileAttributeFunction, currentPage) + }) + + numericTraitsButton := widget.NewButton("Numeric Traits", func(){ + //TODO + showUnderConstructionDialog(window) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, discreteTraitsButton, numericTraitsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_DiscreteGeneticTraits(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + if (userOrOffspring != "User" && userOrOffspring != "Offspring"){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_DiscreteGeneticTraits called with invalid userOrOffspring: " + userOrOffspring), previousPage) + return + } + + if (userOrOffspring == "Offspring"){ + setLoadingScreen(window, "View Profile - Physical", "Computing Genetic Analysis...") + } + + currentPage := func(){setViewMateProfilePage_DiscreteGeneticTraits(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Discrete Genetic Traits")) + + description1 := getLabelCentered("Below is the discrete 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.") @@ -3531,73 +3573,106 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st if (userOrOffspring == newUserOrOffspring){ return } - setViewMateProfilePage_GeneticTraits(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) + setViewMateProfilePage_DiscreteGeneticTraits(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) } userOrOffspringSelector := widget.NewSelect([]string{"User", "Offspring"}, handleSelectButton) - userOrOffspringSelector.Selected = userOrOffspring + userOrOffspringSelector.Selected = userOrOffspring userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) getTraitsInfoGrid := func()(*fyne.Container, error){ - emptyLabelA := widget.NewLabel("") - traitNameLabel := getItalicLabelCentered("Trait Name") - - emptyLabelB := widget.NewLabel("") - userOutcomeScoresLabel := getItalicLabelCentered("User Outcome Scores") - - emptyLabelC := widget.NewLabel("") - offspringOutcomeScoresLabel := getItalicLabelCentered("Offspring Outcome Scores") - - numberOfLabelA := getItalicLabelCentered("Number Of") - rulesTestedLabelA := getItalicLabelCentered("Rules Tested") - - numberOfLabelB := getItalicLabelCentered("Number Of") - rulesTestedLabelB := getItalicLabelCentered("Rules Tested") - - emptyLabelD := widget.NewLabel("") - emptyLabelE := widget.NewLabel("") - - traitNameColumn := container.NewVBox(emptyLabelA, traitNameLabel, widget.NewSeparator()) - userOutcomeScoresColumn := container.NewVBox(emptyLabelB, userOutcomeScoresLabel, widget.NewSeparator()) - offspringOutcomeScoresColumn := container.NewVBox(emptyLabelC, offspringOutcomeScoresLabel, widget.NewSeparator()) - userNumberOfRulesTestedColumn := container.NewVBox(numberOfLabelA, rulesTestedLabelA, widget.NewSeparator()) - offspringNumberOfRulesTestedColumn := container.NewVBox(numberOfLabelB, rulesTestedLabelB, widget.NewSeparator()) - viewTraitDetailsButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) - 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("") + offspringOutcomeProbabilitiesLabel := getItalicLabelCentered("Offspring Outcome Probabilities") + + 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()) + + offspringOutcomeProbabilitiesColumn := container.NewVBox(emptyLabel3, offspringOutcomeProbabilitiesLabel, 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 != "Discrete"){ + continue + } traitRulesList := traitObject.RulesList totalNumberOfTraitRules := len(traitRulesList) - if (totalNumberOfTraitRules == 0){ + traitLociList := traitObject.LociList + + numberOfTraitLoci := len(traitLociList) + + if (totalNumberOfTraitRules == 0 && numberOfTraitLoci == 0){ // We are not able to analyze these traits yet - // We will once we add neural network prediction continue } - totalNumberOfTraitRulesString := helpers.ConvertIntToString(totalNumberOfTraitRules) - - traitOutcomeNamesList := traitObject.OutcomesList - - // We have to sort outcome names so they always show up in the same order - - traitOutcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(traitOutcomeNamesList) + traitNeuralNetworkExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (traitNeuralNetworkExists == false && totalNumberOfTraitRules == 0){ + // We are not able to analyze these traits yet + continue + } traitNameText := getBoldLabelCentered(translate(traitName)) traitNameColumn.Add(traitNameText) viewTraitDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ - setViewMateProfilePage_TraitRules(window, traitName, userOrOffspring, getAnyUserProfileAttributeFunction, currentPage) + if (traitNeuralNetworkExists == true){ + //TODO + showUnderConstructionDialog(window) + } else { + setViewMateProfilePage_DiscreteTraitRules(window, traitName, userOrOffspring, getAnyUserProfileAttributeFunction, currentPage) + } }) viewTraitDetailsButtonsColumn.Add(viewTraitDetailsButton) @@ -3606,8 +3681,6 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st // Map Structure: Locus rsID -> locusValue.LocusValue userTraitLocusValuesMap := make(map[int64]locusValue.LocusValue) - traitLociList := traitObject.LociList - for _, rsID := range traitLociList{ rsIDString := helpers.ConvertInt64ToString(rsID) @@ -3635,200 +3708,244 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st userTraitLocusValuesMap[rsID] = userLocusValue } - //Outputs: - // -bool: At least 1 rule is known - // -map[string]int: Outcome name -> Outcome score - // -int: Number of rules tested - // -error - getUserTraitOutcomeScoresMap := func()(bool, map[string]int, int, error){ - - userTraitOutcomeScoresMap := make(map[string]int) - userNumberOfRulesTested := 0 - - for _, traitRuleObject := range traitRulesList{ - - ruleLociList := traitRuleObject.LociList - - userRuleStatusIsKnown, userPassesRule, err := createPersonGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) - if (err != nil) { return false, nil, 0, err } - if (userRuleStatusIsKnown == false){ - continue - } - userNumberOfRulesTested += 1 - - if (userPassesRule == true){ - - ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap - - for traitOutcome, pointsEffect := range ruleOutcomePointsMap{ - - userTraitOutcomeScoresMap[traitOutcome] += pointsEffect - } - } - } - - if (userNumberOfRulesTested == 0){ - return false, nil, 0, nil - } - - traitOutcomesList := traitObject.OutcomesList - - // We add all outcomes for which there were no points - - for _, traitOutcome := range traitOutcomesList{ - - _, exists := userTraitOutcomeScoresMap[traitOutcome] - if (exists == false){ - userTraitOutcomeScoresMap[traitOutcome] = 0 - } - } - - return true, userTraitOutcomeScoresMap, userNumberOfRulesTested, nil - } - - //Outputs: - // -bool: At least 1 rule is known - // -map[string]float64: Outcome name -> Outcome score - // -int: Number of rules tested - // -error - getOffspringTraitOutcomeScoresMap := func()(bool, map[string]float64, int, error){ - - if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ - // Without my genome person chosen, all offspring rules and outcome scores are unknown - return false, nil, 0, nil - } - - myTraitLocusValuesMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myAnalysisObject, traitName, myGenomeIdentifier) - if (err != nil) { return false, nil, 0, err } - - anyRuleTested, offspringNumberOfRulesTested, _, offspringAverageOutcomeScoresMap, err := createCoupleGeneticAnalysis.GetOffspringTraitInfo(traitObject, myTraitLocusValuesMap, userTraitLocusValuesMap) - if (err != nil) { return false, nil, 0, err } - if (anyRuleTested == false){ - return false, nil, 0, nil - } - - return true, offspringAverageOutcomeScoresMap, offspringNumberOfRulesTested, nil - } - if (userOrOffspring == "User"){ - userTraitOutcomeScoresKnown, userTraitOutcomeScoresMap, userNumberOfRulesTested, err := getUserTraitOutcomeScoresMap() - if (err != nil) { return nil, err } + if (traitNeuralNetworkExists == true){ - numberOfRulesTestedString := helpers.ConvertIntToString(userNumberOfRulesTested) - numberOfRulesTestedFormatted := numberOfRulesTestedString + "/" + totalNumberOfTraitRulesString - numberOfRulesTestedLabel := getBoldLabelCentered(numberOfRulesTestedFormatted) - userNumberOfRulesTestedColumn.Add(numberOfRulesTestedLabel) + traitNeuralNetworkExists, anyLocusValuesAreKnown, predictedOutcome, _, quantityOfLociKnown, _, err := createPersonGeneticAnalysis.GetGenomeDiscreteTraitAnalysis_NeuralNetwork(traitObject, userTraitLocusValuesMap, true) + if (err != nil) { return nil, err } + if (traitNeuralNetworkExists == false){ + return nil, errors.New("GetGenomeTraitAnalysis_NeuralNetwork claims neural network doesn't exist for trait, but we already checked.") + } + if (anyLocusValuesAreKnown == false){ + unknownLabel := getItalicLabelCentered(translate("Unknown")) + userPredictedOutcomeColumn.Add(unknownLabel) + } else { + predictedOutcomeLabel := getBoldLabelCentered(predictedOutcome) + userPredictedOutcomeColumn.Add(predictedOutcomeLabel) + } - if (userTraitOutcomeScoresKnown == false){ - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) + totalNumberOfLociString := helpers.ConvertIntToString(numberOfTraitLoci) + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalNumberOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) - userOutcomeScoresColumn.Add(unknownLabel) } else { - for index, outcomeName := range traitOutcomeNamesListSorted{ + // We use the rules-based analysis - outcomeScore, exists := userTraitOutcomeScoresMap[outcomeName] - if (exists == false){ - return nil, errors.New("Outcome not found in userTraitOutcomeScoresMap.") - } + anyRulesExist, _, quantityOfLociKnown_Rules, _, predictedOutcomeIsKnown, predictedOutcome, err := createPersonGeneticAnalysis.GetGenomeDiscreteTraitAnalysis_Rules(traitObject, userTraitLocusValuesMap, true) + if (err != nil) { return nil, err } + if (anyRulesExist == false){ + return nil, errors.New("GetGenomeTraitAnalysis_Rules claims no rules exist when we already checked.") + } - outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + if (predictedOutcomeIsKnown == false){ + unknownLabel := getItalicLabelCentered("Unknown") + userPredictedOutcomeColumn.Add(unknownLabel) + } else { + predictedOutcomeLabel := getBoldLabelCentered(predictedOutcome) + userPredictedOutcomeColumn.Add(predictedOutcomeLabel) + } - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - userOutcomeScoresColumn.Add(outcomeRow) + traitLociList_Rules := traitObject.LociList_Rules - if (index > 0){ - - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") + totalQuantityOfLoci := len(traitLociList_Rules) - traitNameColumn.Add(emptyLabelA) - userNumberOfRulesTestedColumn.Add(emptyLabelB) - viewTraitDetailsButtonsColumn.Add(emptyLabelC) + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown_Rules) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + } + } else { + + // userOrOffspring == "Offspring" + + if (traitNeuralNetworkExists == true){ + + neuralNetworkExists, anyLociKnown, outcomeProbabilitiesMap, _, quantityOfLociKnown, _, err := createCoupleGeneticAnalysis.GetOffspringDiscreteTraitInfo_NeuralNetwork(traitObject, userTraitLocusValuesMap, myGenomeLocusValuesMap) + if (err != nil) { return nil, err } + if (neuralNetworkExists == false){ + return nil, errors.New("GetOffspringTraitInfo_NeuralNetwork claiming that neural network doesn't exist when we already checked.") + } + + totalNumberOfLociString := helpers.ConvertIntToString(numberOfTraitLoci) + + quantityOfLociKnownString := helpers.ConvertIntToString(quantityOfLociKnown) + + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalNumberOfLociString + + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + + if (anyLociKnown == false){ + unknownLabel := getItalicLabelCentered("Unknown") + + offspringOutcomeProbabilitiesColumn.Add(unknownLabel) + + } else { + + outcomesList := helpers.GetListOfMapKeys(outcomeProbabilitiesMap) + + // We sort the outcomes in alphabetical order so they show up the same way each time + slices.Sort(outcomesList) + + quantityOfAddedItems := 0 + + for _, outcomeName := range outcomesList{ + + outcomeProbability, exists := outcomeProbabilitiesMap[outcomeName] + if (exists == false){ + return nil, errors.New("GetListOfMapKeys returning element which doesn't exist in map.") + } + if (outcomeProbability == 0){ + continue + } + + outcomeProbabilityString := helpers.ConvertIntToString(outcomeProbability) + outcomeProbabilityLabelText := outcomeName + ": " + outcomeProbabilityString + "%" + + outcomeProbabilityLabel := getBoldLabelCentered(outcomeProbabilityLabelText) + + offspringOutcomeProbabilitiesColumn.Add(outcomeProbabilityLabel) + + quantityOfAddedItems += 1 + + if (quantityOfAddedItems > 1){ + // We add whitespace for the other columns + traitNameColumn.Add(widget.NewLabel("")) + quantityOfLociKnownColumn.Add(widget.NewLabel("")) + viewTraitDetailsButtonsColumn.Add(widget.NewLabel("")) + } } } - } - } else if (userOrOffspring == "Offspring"){ - - offspringTraitOutcomeScoresKnown, offspringTraitOutcomeScoresMap, offspringNumberOfRulesTested, err := getOffspringTraitOutcomeScoresMap() - if (err != nil) { return nil, err } - - numberOfRulesTestedString := helpers.ConvertIntToString(offspringNumberOfRulesTested) - numberOfRulesTestedFormatted := numberOfRulesTestedString + "/" + totalNumberOfTraitRulesString - numberOfRulesTestedLabel := getBoldLabelCentered(numberOfRulesTestedFormatted) - offspringNumberOfRulesTestedColumn.Add(numberOfRulesTestedLabel) - - if (offspringTraitOutcomeScoresKnown == false){ - unknownTranslated := translate("Unknown") - unknownLabel := getBoldLabelCentered(unknownTranslated) - - offspringOutcomeScoresColumn.Add(unknownLabel) } else { - for index, outcomeName := range traitOutcomeNamesListSorted{ + // We use the rules-based analysis - outcomeScore, exists := offspringTraitOutcomeScoresMap[outcomeName] - if (exists == false){ - return nil, errors.New("Outcome not found in offspringTraitOutcomeScoresMap.") - } + anyRulesExist, rulesAnalysisExists, _, offspringQuantityOfLociKnown, _, outcomeProbabilitiesMap, err := createCoupleGeneticAnalysis.GetOffspringDiscreteTraitInfo_Rules(traitObject, myGenomeLocusValuesMap, userTraitLocusValuesMap) + if (err != nil) { return nil, err } + if (anyRulesExist == false){ + return nil, errors.New("GetOffspringDiscreteTraitInfo_Rules claiming that no rules exist when we already checked.") + } - outcomeScoreString := helpers.ConvertFloat64ToStringRounded(outcomeScore, 2) + lociList_Rules := traitObject.LociList_Rules - outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) - offspringOutcomeScoresColumn.Add(outcomeRow) + totalQuantityOfLoci := len(lociList_Rules) - if (index > 0){ - - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") - emptyLabelC := widget.NewLabel("") + quantityOfLociKnownString := helpers.ConvertIntToString(offspringQuantityOfLociKnown) + totalQuantityOfLociString := helpers.ConvertIntToString(totalQuantityOfLoci) - traitNameColumn.Add(emptyLabelA) - offspringNumberOfRulesTestedColumn.Add(emptyLabelB) - viewTraitDetailsButtonsColumn.Add(emptyLabelC) + quantityOfLociKnownFormatted := quantityOfLociKnownString + "/" + totalQuantityOfLociString + quantityOfLociKnownLabel := getBoldLabelCentered(quantityOfLociKnownFormatted) + quantityOfLociKnownColumn.Add(quantityOfLociKnownLabel) + + if (rulesAnalysisExists == false){ + unknownLabel := getItalicLabelCentered(translate("Unknown")) + + offspringOutcomeProbabilitiesColumn.Add(unknownLabel) + } else { + + outcomesList := helpers.GetListOfMapKeys(outcomeProbabilitiesMap) + + // We sort the outcomes in alphabetical order so they show up the same way each time + slices.Sort(outcomesList) + + quantityOfAddedItems := 0 + + for _, outcomeName := range outcomesList{ + + outcomeProbability, exists := outcomeProbabilitiesMap[outcomeName] + if (exists == false){ + return nil, errors.New("GetListOfMapKeys returning element which doesn't exist in map.") + } + + if (outcomeProbability == 0){ + continue + } + + outcomeProbabilityString := helpers.ConvertIntToString(outcomeProbability) + outcomeProbabilityLabelText := outcomeName + ": " + outcomeProbabilityString + "%" + + outcomeProbabilityLabel := getBoldLabelCentered(outcomeProbabilityLabelText) + + offspringOutcomeProbabilitiesColumn.Add(outcomeProbabilityLabel) + + quantityOfAddedItems += 1 + + if (quantityOfAddedItems > 1){ + // We add whitespace for the other columns + traitNameColumn.Add(widget.NewLabel("")) + quantityOfLociKnownColumn.Add(widget.NewLabel("")) + viewTraitDetailsButtonsColumn.Add(widget.NewLabel("")) + } } } } } traitNameColumn.Add(widget.NewSeparator()) - userOutcomeScoresColumn.Add(widget.NewSeparator()) - offspringOutcomeScoresColumn.Add(widget.NewSeparator()) - userNumberOfRulesTestedColumn.Add(widget.NewSeparator()) - offspringNumberOfRulesTestedColumn.Add(widget.NewSeparator()) + userPredictedOutcomeColumn.Add(widget.NewSeparator()) + offspringOutcomeProbabilitiesColumn.Add(widget.NewSeparator()) + quantityOfLociKnownColumn.Add(widget.NewSeparator()) viewTraitDetailsButtonsColumn.Add(widget.NewSeparator()) } - userOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitOutcomeScoresExplainerPage(window, currentPage) + if (userOrOffspring == "User"){ + + predictedOutcomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + //TODO + showUnderConstructionDialog(window) + }) + + userPredictedOutcomeColumn.Add(predictedOutcomeHelpButton) + + } else { + // userOrOffspring == "Offspring" + + outcomeProbabilitiesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + //TODO + showUnderConstructionDialog(window) + }) + + offspringOutcomeProbabilitiesColumn.Add(outcomeProbabilitiesHelpButton) + } + + quantityOfLociKnownHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) }) - offspringOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) - }) + quantityOfLociKnownColumn.Add(quantityOfLociKnownHelpButton) - userNumberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitNumberOfRulesTestedExplainerPage(window, currentPage) - }) - - offspringNumberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) - }) - - userOutcomeScoresColumn.Add(userOutcomeScoresHelpButton) - offspringOutcomeScoresColumn.Add(offspringOutcomeScoresHelpButton) - userNumberOfRulesTestedColumn.Add(userNumberOfRulesTestedHelpButton) - offspringNumberOfRulesTestedColumn.Add(offspringNumberOfRulesTestedHelpButton) + traitsInfoGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn) if (userOrOffspring == "User"){ - traitsInfoGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, userOutcomeScoresColumn, userNumberOfRulesTestedColumn, viewTraitDetailsButtonsColumn, layout.NewSpacer()) + + traitsInfoGrid.Add(userPredictedOutcomeColumn) - return traitsInfoGrid, nil + } else { + + // userOrOffspring == "Offspring" + + traitsInfoGrid.Add(offspringOutcomeProbabilitiesColumn) } - traitsInfoGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, offspringOutcomeScoresColumn, offspringNumberOfRulesTestedColumn, viewTraitDetailsButtonsColumn, layout.NewSpacer()) + + traitsInfoGrid.Add(quantityOfLociKnownColumn) + traitsInfoGrid.Add(viewTraitDetailsButtonsColumn) + traitsInfoGrid.Add(layout.NewSpacer()) return traitsInfoGrid, nil } @@ -3844,9 +3961,9 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st setPageContent(page, window) } -func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ +func setViewMateProfilePage_DiscreteTraitRules(window fyne.Window, traitName string, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ - currentPage := func(){setViewMateProfilePage_TraitRules(window, traitName, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + currentPage := func(){setViewMateProfilePage_DiscreteTraitRules(window, traitName, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} title := getPageTitleCentered("View Profile - Physical") @@ -3868,7 +3985,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use // -bool: Any trait locus value exists for this myself // -map[int64]locusValue.LocusValue: My locus values map // -error - getMyTraitLocusValuesMap := func()(bool, map[int64]locusValue.LocusValue, error){ + getMyGenomeLocusValuesMap := func()(bool, map[int64]locusValue.LocusValue, error){ myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() if (err != nil) { return false, nil, err } @@ -3878,17 +3995,18 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use return false, nil, nil } - myTraitLocusValuesMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myAnalysisObject, traitName, myGenomeIdentifier) - if (err != nil) { return false, nil, err } + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myAnalysisObject) + if (err != nil){ return false, nil, err } - if (len(myTraitLocusValuesMap) == 0){ - return false, nil, nil + myGenomeMap, exists := myGenomesMap[myGenomeIdentifier] + if (exists == false){ + return false, nil, errors.New("GetMyChosenMateGeneticAnalysis returning analysis which does not contain genome matching myGenomeIdentifier") } - return true, myTraitLocusValuesMap, nil + return true, myGenomeMap, nil } - anyMyTraitLocusValuesExist, myTraitLocusValuesMap, err := getMyTraitLocusValuesMap() + anyMyLocusValuesExist, myLocusValuesMap, err := getMyGenomeLocusValuesMap() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return @@ -3900,9 +4018,14 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use return } - traitLociList := traitObject.LociList + traitLociList_Rules := traitObject.LociList_Rules traitRulesList := traitObject.RulesList + if (len(traitRulesList) == 0){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_DiscreteTraitRules called with trait which has no rules."), previousPage) + return + } + //Outputs: // -bool: Any trait locus value exists for this user // -map[int64]locusValue.LocusValue: User locus values map @@ -3913,7 +4036,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use // Map Structure: Locus rsID -> locusValue.LocusValue userTraitLocusValuesMap := make(map[int64]locusValue.LocusValue) - for _, rsID := range traitLociList{ + for _, rsID := range traitLociList_Rules{ rsIDString := helpers.ConvertInt64ToString(rsID) @@ -3959,12 +4082,15 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use // -error getOffspringProbabilityOfPassingRulesMap := func()(bool, map[[3]byte]int, error){ - if (anyMyTraitLocusValuesExist == false || anyUserTraitLocusValueExists == false){ + if (anyMyLocusValuesExist == false || anyUserTraitLocusValueExists == false){ return false, nil, nil } - anyOffspringRulesTested, _, offspringProbabilityOfPassingRulesMap, _, err := createCoupleGeneticAnalysis.GetOffspringTraitInfo(traitObject, myTraitLocusValuesMap, userTraitLocusValuesMap) + anyRulesExist, anyOffspringRulesTested, _, _, offspringProbabilityOfPassingRulesMap, _, err := createCoupleGeneticAnalysis.GetOffspringDiscreteTraitInfo_Rules(traitObject, myLocusValuesMap, userTraitLocusValuesMap) if (err != nil) { return false, nil, err } + if (anyRulesExist == false){ + return false, nil, errors.New("GetOffspringDiscreteTraitInfo claiming no trait rules exist when we already checked.") + } if (anyOffspringRulesTested == false){ return false, nil, nil } @@ -4000,7 +4126,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use ruleLociList := ruleObject.LociList - ruleStatusIsKnown, _, err := createPersonGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) + ruleStatusIsKnown, _, err := createPersonGeneticAnalysis.GetGenomePassesDiscreteTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) if (err != nil) { return 0, err } if (ruleStatusIsKnown == true){ numberOfRulesTested += 1 @@ -4027,9 +4153,9 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use numberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ if (userOrOffspring == "User"){ - setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + setDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) } else { - setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + setOffspringDiscreteTraitQuantityOfRulesTestedExplainerPage(window, currentPage) } }) @@ -4093,7 +4219,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use getUserPassesRuleString := func()(string, error){ - userRuleStatusIsKnown, userPassesRule, err := createPersonGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) + userRuleStatusIsKnown, userPassesRule, err := createPersonGeneticAnalysis.GetGenomePassesDiscreteTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) if (err != nil) { return "", err } if (userRuleStatusIsKnown == false){ @@ -4134,7 +4260,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use // We do this because the rule effects column may be multiple rows tall viewRuleInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewTraitRuleDetailsPage(window, traitName, ruleIdentifierHex, currentPage) + setViewDiscreteTraitRuleDetailsPage(window, traitName, ruleIdentifierHex, currentPage) }) ruleIdentifierLabel := getBoldLabelCentered(ruleIdentifierHex) userPassesRuleLabel := getBoldLabelCentered(userPassesRuleString) @@ -4200,11 +4326,11 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use } ruleEffectsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + setDiscreteTraitRuleOutcomeEffectsExplainerPage(window, currentPage) }) userPassesRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ - setPersonPassesTraitRuleExplainerPage(window, currentPage) + setPersonPassesDiscreteTraitRuleExplainerPage(window, currentPage) }) offspringProbabilityOfPassingRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ @@ -4491,29 +4617,29 @@ func get23andMeAncestryCompositionDisplay(inputContinentPercentagesMap map[strin // We sort region subregions list // We sort them from highest to lowest in percentage. - compareSubregionsFunction := func(subregionADescription string, subregionBDescription string)int{ + compareSubregionsFunction := func(subregion1Description string, subregion2Description string)int{ - if (subregionADescription == subregionBDescription){ + if (subregion1Description == subregion2Description){ panic("compareSubregionsFunction called with identical subregion descriptions.") } - subregionAPercentage, exists := subregionDescriptionPercentagesMap[subregionADescription] + subregion1Percentage, exists := subregionDescriptionPercentagesMap[subregion1Description] if (exists == false){ panic("subregionPercentagesMap missing subregion during sort.") } - subregionBPercentage, exists := subregionDescriptionPercentagesMap[subregionBDescription] + subregion2Percentage, exists := subregionDescriptionPercentagesMap[subregion2Description] if (exists == false){ panic("subregionPercentagesMap missing subregion during sort.") } - if (subregionAPercentage == subregionBPercentage){ + if (subregion1Percentage == subregion2Percentage){ // We sort subregions in unicode order - if (subregionADescription < subregionBDescription){ + if (subregion1Description < subregion2Description){ return -1 } return 1 } - if (subregionAPercentage > subregionBPercentage){ + if (subregion1Percentage > subregion2Percentage){ return -1 } @@ -4527,32 +4653,33 @@ func get23andMeAncestryCompositionDisplay(inputContinentPercentagesMap map[strin // We sort continent regions list by highest to lowest percentage. - compareRegionsFunction := func(regionADescription string, regionBDescription string)int{ + compareRegionsFunction := func(region1Description string, region2Description string)int{ - if (regionADescription == regionBDescription){ + if (region1Description == region2Description){ panic("compareRegionsFunction called with identical regions.") } - regionAPercentage, exists := regionDescriptionPercentagesMap[regionADescription] + region1Percentage, exists := regionDescriptionPercentagesMap[region1Description] if (exists == false){ panic("regionPercentagesMap missing subregion during sort.") } - - regionBPercentage, exists := regionDescriptionPercentagesMap[regionBDescription] + + region2Percentage, exists := regionDescriptionPercentagesMap[region2Description] if (exists == false){ panic("regionPercentagesMap missing subregion during sort.") } - if (regionAPercentage == regionBPercentage){ + if (region1Percentage == region2Percentage){ // We sort regions in unicode order - if (regionADescription < regionBDescription){ + if (region1Description < region2Description){ return -1 } return 1 } - if (regionAPercentage > regionBPercentage){ + if (region1Percentage > region2Percentage){ return -1 } + return 1 } @@ -4563,29 +4690,29 @@ func get23andMeAncestryCompositionDisplay(inputContinentPercentagesMap map[strin // We sort root list by highest to lowest proportions - compareContinentsFunction := func(continentADescription string, continentBDescription string)int{ + compareContinentsFunction := func(continent1Description string, continent2Description string)int{ - if (continentADescription == continentBDescription){ + if (continent1Description == continent2Description){ panic("compareContinentsFunction called with identical continents.") } - continentAPercentage, exists := continentDescriptionPercentagesMap[continentADescription] + continent1Percentage, exists := continentDescriptionPercentagesMap[continent1Description] if (exists == false){ panic("Continent percentage not found when sorting root list.") } - continentBPercentage, exists := continentDescriptionPercentagesMap[continentBDescription] + continent2Percentage, exists := continentDescriptionPercentagesMap[continent2Description] if (exists == false){ panic("Continent percentage not found when sorting root list.") } - if (continentAPercentage == continentBPercentage){ + if (continent1Percentage == continent2Percentage){ // We sort continents in unicode order - if (continentADescription < continentBDescription){ + if (continent1Description < continent2Description){ return -1 } return 1 } - if (continentAPercentage > continentBPercentage){ + if (continent1Percentage > continent2Percentage){ return -1 } return 1 diff --git a/internal/generate/generate.go b/internal/generate/generate.go index 24ac348..ad78b7e 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -932,7 +932,7 @@ func GetFakeProfile(profileType string, identityPublicKey [32]byte, identityPriv traitLociList := traitObject.LociList - for _, rsID := range traitLociList{ + for _, rsID := range traitLociList{ shareableRSIDsMap[rsID] = struct{}{} } diff --git a/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go b/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go index bfeeaab..f179d89 100644 --- a/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go +++ b/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go @@ -9,21 +9,10 @@ package createCoupleGeneticAnalysis // Disclaimer: I am a novice in the ways of genetics. This package could be flawed in numerous ways. -// TODO: We want to eventually use neural nets for both trait and polygenic disease analysis (see geneticPrediction.go) -// These will be trained on a set of genomes and will output a probability analysis for each trait/disease +// TODO: We want to eventually use neural nets for polygenic disease analysis (see geneticPrediction.go) // This is only possible once we get access to the necessary training data -// -// This is how offspring trait prediction could work with the neural net model: -// Both users will share all relevant SNPS base pairs that determine the trait on their profile. -// Each location has 4 possible outcomes, so for 1000 SNPs, there are 4^1000 possible offspring outcomes for a given couple. (this -// is actually too high because recombination break points do not occur at each locus, see genetic linkage) -// This is too many options for us to check all of them. -// Seekia will create 100 offspring that would be produced from both users, and run each offspring through the neural net. -// Each offspring would be different. The allele from each parent for each SNP would be randomly chosen. -// The user can choose how many prospective offspring to create in the settings. -// More offspring will take longer, but will yield a more accurate trait probability. -// Seekia will show the the average trait result and a chart showing the trait results for all created offspring. +import "seekia/resources/geneticPredictionModels" import "seekia/resources/geneticReferences/locusMetadata" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" @@ -40,6 +29,7 @@ import "errors" import mathRand "math/rand/v2" import "slices" import "maps" +import "reflect" //Outputs: @@ -86,6 +76,30 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom return false, "", nil } + // This map stores each genome's locus values + // Map Structure: Genome Identifier -> Genome locus values map (rsID -> Locus Value) + person1GenomesMap := make(map[[16]byte]map[int64]locusValue.LocusValue) + + for _, genomeWithMetadata := range person1GenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadata.GenomeIdentifier + genomeMap := genomeWithMetadata.GenomeMap + + person1GenomesMap[genomeIdentifier] = genomeMap + } + + // This map stores each genome's locus values + // Map Structure: Genome Identifier -> Genome locus values map (rsID -> Locus Value) + person2GenomesMap := make(map[[16]byte]map[int64]locusValue.LocusValue) + + for _, genomeWithMetadata := range person2GenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadata.GenomeIdentifier + genomeMap := genomeWithMetadata.GenomeMap + + person2GenomesMap[genomeIdentifier] = genomeMap + } + // The analysis will analyze either 1 or 2 genome pairs // The gui will display the results from each pair //Outputs: @@ -172,9 +186,9 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom person2DiseaseAnalysisObject, err := createPersonGeneticAnalysis.GetPersonMonogenicDiseaseAnalysis(person2GenomesWithMetadataList, diseaseObject) if (err != nil) { return false, "", err } - // This map stores the number of variants tested in each person's genome + // This map stores the quantity of variants tested in each person's genome // Map Structure: Genome Identifier -> Number of variants tested - numberOfVariantsTestedMap := make(map[[16]byte]int) + quantityOfVariantsTestedMap := make(map[[16]byte]int) // This map stores the offspring disease probabilities for each genome pair. // A genome pair is a concatenation of two genome identifiers @@ -184,7 +198,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom // This will calculate the probability of monogenic disease for the offspring from the two specified genomes // It also calculates the probabilities for each monogenic disease variant for the offspring - // It then adds the genome pair disease information to the offspringMonogenicDiseaseInfoMap and numberOfVariantsTestedMap + // It then adds the genome pair disease information to the offspringMonogenicDiseaseInfoMap and quantityOfVariantsTestedMap addGenomePairInfoToDiseaseMaps := func(person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ //Outputs: @@ -202,9 +216,9 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom personGenomeProbabilityOfPassingADiseaseVariant := personGenomeDiseaseInfoObject.ProbabilityOfPassingADiseaseVariant - personGenomeNumberOfVariantsTested := personGenomeDiseaseInfoObject.NumberOfVariantsTested + personGenomeQuantityOfVariantsTested := personGenomeDiseaseInfoObject.QuantityOfVariantsTested - return true, personGenomeProbabilityOfPassingADiseaseVariant, personGenomeNumberOfVariantsTested + return true, personGenomeProbabilityOfPassingADiseaseVariant, personGenomeQuantityOfVariantsTested } person1ProbabilityIsKnown, person1WillPassVariantProbability, person1NumberOfVariantsTested := getPersonWillPassDiseaseVariantProbability(person1DiseaseAnalysisObject, person1GenomeIdentifier) @@ -362,8 +376,8 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom return nil } - numberOfVariantsTestedMap[person1GenomeIdentifier] = person1NumberOfVariantsTested - numberOfVariantsTestedMap[person2GenomeIdentifier] = person2NumberOfVariantsTested + quantityOfVariantsTestedMap[person1GenomeIdentifier] = person1NumberOfVariantsTested + quantityOfVariantsTestedMap[person2GenomeIdentifier] = person2NumberOfVariantsTested newOffspringGenomePairMonogenicDiseaseInfoObject := geneticAnalysis.OffspringGenomePairMonogenicDiseaseInfo{ ProbabilityOffspringHasDiseaseIsKnown: offspringHasDiseaseProbabilityIsKnown, @@ -390,7 +404,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom } newOffspringMonogenicDiseaseInfoObject := geneticAnalysis.OffspringMonogenicDiseaseInfo{ - NumberOfVariantsTestedMap: numberOfVariantsTestedMap, + QuantityOfVariantsTestedMap: quantityOfVariantsTestedMap, MonogenicDiseaseInfoMap: offspringMonogenicDiseaseInfoMap, } @@ -480,12 +494,6 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom diseaseName := diseaseObject.DiseaseName diseaseLociList := diseaseObject.LociList - person1DiseaseAnalysisObject, err := createPersonGeneticAnalysis.GetPersonPolygenicDiseaseAnalysis(person1GenomesWithMetadataList, diseaseObject) - if (err != nil) { return false, "", err } - - person2DiseaseAnalysisObject, err := createPersonGeneticAnalysis.GetPersonPolygenicDiseaseAnalysis(person2GenomesWithMetadataList, diseaseObject) - if (err != nil) { return false, "", err } - // This map stores the polygenic disease info for each genome pair // Map Structure: Genome Pair Identifier -> OffspringGenomePairPolygenicDiseaseInfo offspringPolygenicDiseaseInfoMap := make(map[[32]byte]geneticAnalysis.OffspringGenomePairPolygenicDiseaseInfo) @@ -494,40 +502,17 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom // It then adds the pair entry to the offspringPolygenicDiseaseInfoMap addGenomePairDiseaseInfoToDiseaseMap := func(person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ - //Outputs: - // -bool: Any locus values exist - // -map[int64]locusValue.LocusValue - // -error - getPersonGenomeDiseaseLocusValuesMap := func(personGenomeIdentifier [16]byte, personDiseaseAnalysisObject geneticAnalysis.PersonPolygenicDiseaseInfo)(bool, map[int64]locusValue.LocusValue, error){ - - personPolygenicDiseaseInfoMap := personDiseaseAnalysisObject.PolygenicDiseaseInfoMap - - personGenomeDiseaseInfoObject, exists := personPolygenicDiseaseInfoMap[personGenomeIdentifier] - if (exists == false){ - // This person's genome has no information about loci related to this disease - return false, nil, nil - } - - personGenomeLocusValuesMap := personGenomeDiseaseInfoObject.LocusValuesMap - - return true, personGenomeLocusValuesMap, nil + person1LocusValuesMap, exists := person1GenomesMap[person1GenomeIdentifier] + if (exists == false){ + return errors.New("addGenomePairDiseaseInfoToDiseaseMap called with unknown person1GenomeIdentifier.") } - anyPerson1LociValuesExist, person1LocusValuesMap, err := getPersonGenomeDiseaseLocusValuesMap(person1GenomeIdentifier, person1DiseaseAnalysisObject) - if (err != nil) { return err } - if (anyPerson1LociValuesExist == false){ - // Offspring's disease info for this locus on this genome pair is unknown - return nil + person2LocusValuesMap, exists := person2GenomesMap[person2GenomeIdentifier] + if (exists == false){ + return errors.New("addGenomePairDiseaseInfoToDiseaseMap called with unknown person2GenomeIdentifier.") } - anyPerson2LociValuesExist, person2LocusValuesMap, err := getPersonGenomeDiseaseLocusValuesMap(person2GenomeIdentifier, person2DiseaseAnalysisObject) - if (err != nil) { return err } - if (anyPerson2LociValuesExist == false){ - // Offspring's disease info for this locus on this genome pair is unknown - return nil - } - - anyOffspringLocusTested, genomePairOffspringAverageRiskScore, numberOfLociTested, genomePairOffspringDiseaseLociInfoMap, genomePairSampleOffspringRiskScoresList, err := GetOffspringPolygenicDiseaseInfo(diseaseLociList, person1LocusValuesMap, person2LocusValuesMap) + anyOffspringLocusTested, genomePairOffspringAverageRiskScore, quantityOfLociTested, genomePairOffspringDiseaseLociInfoMap, genomePairSampleOffspringRiskScoresList, err := GetOffspringPolygenicDiseaseInfo(diseaseLociList, person1LocusValuesMap, person2LocusValuesMap) if (err != nil) { return err } if (anyOffspringLocusTested == false){ // We have no information about this genome pair's disease risk @@ -537,7 +522,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom newOffspringGenomePairPolygenicDiseaseInfo := geneticAnalysis.OffspringGenomePairPolygenicDiseaseInfo{ - NumberOfLociTested: numberOfLociTested, + QuantityOfLociTested: quantityOfLociTested, OffspringAverageRiskScore: genomePairOffspringAverageRiskScore, LociInfoMap: genomePairOffspringDiseaseLociInfoMap, SampleOffspringRiskScoresList: genomePairSampleOffspringRiskScoresList, @@ -570,34 +555,20 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom checkIfConflictExists := func()(bool, error){ - numberOfLociTested := 0 - offspringAverageRiskScore := 0 - offspringLociInfoMap := make(map[[3]byte]geneticAnalysis.OffspringPolygenicDiseaseLocusInfo) + currentGenomePairPolygenicDiseaseInfo := geneticAnalysis.OffspringGenomePairPolygenicDiseaseInfo{} firstItemReached := false for _, genomePairDiseaseInfoObject := range offspringPolygenicDiseaseInfoMap{ - genomePairNumberOfLociTested := genomePairDiseaseInfoObject.NumberOfLociTested - genomePairOffspringAverageRiskScore := genomePairDiseaseInfoObject.OffspringAverageRiskScore - genomePairLociInfoMap := genomePairDiseaseInfoObject.LociInfoMap - if (firstItemReached == false){ - numberOfLociTested = genomePairNumberOfLociTested - offspringAverageRiskScore = genomePairOffspringAverageRiskScore - offspringLociInfoMap = genomePairLociInfoMap + currentGenomePairPolygenicDiseaseInfo = genomePairDiseaseInfoObject firstItemReached = true continue } - if (numberOfLociTested != genomePairNumberOfLociTested){ - return true, nil - } - if (offspringAverageRiskScore != genomePairOffspringAverageRiskScore){ - return true, nil - } - areEqual := maps.Equal(offspringLociInfoMap, genomePairLociInfoMap) + + areEqual := reflect.DeepEqual(genomePairDiseaseInfoObject, currentGenomePairPolygenicDiseaseInfo) if (areEqual == false){ - // A conflict exists return true, nil } } @@ -622,128 +593,140 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom if (err != nil) { return false, "", err } // Map Structure: Trait Name -> Trait Info Object - offspringTraitsMap := make(map[string]geneticAnalysis.OffspringTraitInfo) + offspringDiscreteTraitsMap := make(map[string]geneticAnalysis.OffspringDiscreteTraitInfo) + + // Map Structure: Trait Name -> Trait Info Object + offspringNumericTraitsMap := make(map[string]geneticAnalysis.OffspringNumericTraitInfo) for _, traitObject := range traitObjectsList{ traitName := traitObject.TraitName + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric - person1TraitAnalysisObject, err := createPersonGeneticAnalysis.GetPersonTraitAnalysis(person1GenomesWithMetadataList, traitObject) - if (err != nil) { return false, "", err } + if (traitIsDiscreteOrNumeric == "Discrete"){ - person2TraitAnalysisObject, err := createPersonGeneticAnalysis.GetPersonTraitAnalysis(person2GenomesWithMetadataList, traitObject) - if (err != nil) { return false, "", err } + // This map stores the trait info for each genome pair + // Map Structure: Genome Pair Identifier -> OffspringGenomePairDiscreteTraitInfo + offspringTraitInfoMap := make(map[[32]byte]geneticAnalysis.OffspringGenomePairDiscreteTraitInfo) - // This map stores the trait info for each genome pair - // Map Structure: Genome Pair Identifier -> OffspringGenomePairTraitInfo - offspringTraitInfoMap := make(map[[32]byte]geneticAnalysis.OffspringGenomePairTraitInfo) + // This will add the offspring trait information for the provided genome pair to the offspringTraitInfoMap + addGenomePairTraitInfoToOffspringMap := func(person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ - // 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.") + } - person1TraitInfoMap := person1TraitAnalysisObject.TraitInfoMap - person2TraitInfoMap := person2TraitAnalysisObject.TraitInfoMap + person2LocusValuesMap, exists := person2GenomesMap[person2GenomeIdentifier] + if (exists == false){ + return errors.New("addGenomePairTraitInfoToOffspringMap called with unknown person2GenomeIdentifier.") + } - person1GenomeTraitInfoObject, exists := person1TraitInfoMap[person1GenomeIdentifier] - if (exists == false){ - // This person has no genome values for any loci for this trait - // No predictions are possible - return nil - } - person2GenomeTraitInfoObject, exists := person2TraitInfoMap[person2GenomeIdentifier] - if (exists == false){ - // This person has no genome values for any loci for this trait - // No predictions are possible - return nil - } + newOffspringGenomePairTraitInfo := geneticAnalysis.OffspringGenomePairDiscreteTraitInfo{} - person1LocusValuesMap := person1GenomeTraitInfoObject.LocusValuesMap - person2LocusValuesMap := person2GenomeTraitInfoObject.LocusValuesMap + neuralNetworkExists, neuralNetworkAnalysisExists, outcomeProbabilitiesMap, averagePredictionConfidence, quantityOfLociTested, quantityOfParentalPhasedLoci, err := GetOffspringDiscreteTraitInfo_NeuralNetwork(traitObject, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return err } + if (neuralNetworkExists == true){ - anyRulesTested, numberOfRulesTested, offspringProbabilityOfPassingRulesMap, offspringAverageOutcomeScoresMap, err := GetOffspringTraitInfo(traitObject, person1LocusValuesMap, person2LocusValuesMap) - if (err != nil) { return err } - if (anyRulesTested == false){ - // No rules were tested for this trait - // We will not add anything to the trait info map for this genome pair - return nil - } + newOffspringGenomePairTraitInfo.NeuralNetworkExists = true - newOffspringGenomePairTraitInfoObject := geneticAnalysis.OffspringGenomePairTraitInfo{ - NumberOfRulesTested: numberOfRulesTested, - OffspringAverageOutcomeScoresMap: offspringAverageOutcomeScoresMap, - ProbabilityOfPassingRulesMap: offspringProbabilityOfPassingRulesMap, - } + if (neuralNetworkAnalysisExists == true){ + newOffspringGenomePairTraitInfo.NeuralNetworkAnalysisExists = true - genomePairIdentifier := helpers.JoinTwo16ByteArrays(person1GenomeIdentifier, person2GenomeIdentifier) + newOffspringGenomePairTraitInfo_NeuralNetwork := geneticAnalysis.OffspringGenomePairDiscreteTraitInfo_NeuralNetwork{ - offspringTraitInfoMap[genomePairIdentifier] = newOffspringGenomePairTraitInfoObject + OffspringOutcomeProbabilitiesMap: outcomeProbabilitiesMap, + AverageConfidence: averagePredictionConfidence, + QuantityOfLociKnown: quantityOfLociTested, + QuantityOfParentalPhasedLoci: quantityOfParentalPhasedLoci, + } - 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.OffspringTraitInfo{ - 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 - - offspringAverageOutcomeScoresMap := make(map[string]float64) - offspringProbabilityOfPassingRulesMap := make(map[[3]byte]int) - - firstItemReached := false - - for _, genomePairTraitInfoObject := range offspringTraitInfoMap{ - - currentOffspringAverageOutcomeScoresMap := genomePairTraitInfoObject.OffspringAverageOutcomeScoresMap - currentProbabilityOfPassingRulesMap := genomePairTraitInfoObject.ProbabilityOfPassingRulesMap - - if (firstItemReached == false){ - offspringAverageOutcomeScoresMap = currentOffspringAverageOutcomeScoresMap - offspringProbabilityOfPassingRulesMap = currentProbabilityOfPassingRulesMap - - firstItemReached = true - continue - } - - areEqual := maps.Equal(offspringAverageOutcomeScoresMap, currentOffspringAverageOutcomeScoresMap) - if (areEqual == false){ - return true, nil - } - areEqual = maps.Equal(offspringProbabilityOfPassingRulesMap, currentProbabilityOfPassingRulesMap) - if (areEqual == false){ - return true, nil + newOffspringGenomePairTraitInfo.NeuralNetworkAnalysis = newOffspringGenomePairTraitInfo_NeuralNetwork } } - return false, nil + anyRulesExist, rulesAnalysisExists, quantityOfRulesTested, quantityOfLociKnown, offspringProbabilityOfPassingRulesMap, offspringOutcomeProbabilitiesMap, err := GetOffspringDiscreteTraitInfo_Rules(traitObject, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return err } + if (anyRulesExist == true){ + + newOffspringGenomePairTraitInfo.RulesExist = true + + if (rulesAnalysisExists == true){ + newOffspringGenomePairTraitInfo.RulesAnalysisExists = true + + newOffspringGenomePairTraitInfo_Rules := geneticAnalysis.OffspringGenomePairDiscreteTraitInfo_Rules{ + QuantityOfRulesTested: quantityOfRulesTested, + QuantityOfLociKnown: quantityOfLociKnown, + ProbabilityOfPassingRulesMap: offspringProbabilityOfPassingRulesMap, + OffspringOutcomeProbabilitiesMap: offspringOutcomeProbabilitiesMap, + } + + newOffspringGenomePairTraitInfo.RulesAnalysis = newOffspringGenomePairTraitInfo_Rules + } + } + + genomePairIdentifier := helpers.JoinTwo16ByteArrays(person1GenomeIdentifier, person2GenomeIdentifier) + + offspringTraitInfoMap[genomePairIdentifier] = newOffspringGenomePairTraitInfo + + return nil } - conflictExists, err := checkIfConflictExists() + err = addGenomePairTraitInfoToOffspringMap(pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier) if (err != nil) { return false, "", err } - newOffspringTraitInfoObject.ConflictExists = conflictExists - } + if (genomePair2Exists == true){ - offspringTraitsMap[traitName] = newOffspringTraitInfoObject + err := addGenomePairTraitInfoToOffspringMap(pair2Person1GenomeIdentifier, pair2Person2GenomeIdentifier) + if (err != nil) { return false, "", err } + } + + newOffspringTraitInfoObject := geneticAnalysis.OffspringDiscreteTraitInfo{ + 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.OffspringGenomePairDiscreteTraitInfo{} + + 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 + } + + offspringDiscreteTraitsMap[traitName] = newOffspringTraitInfoObject + } } - newCoupleAnalysis.TraitsMap = offspringTraitsMap + newCoupleAnalysis.DiscreteTraitsMap = offspringDiscreteTraitsMap + newCoupleAnalysis.NumericTraitsMap = offspringNumericTraitsMap analysisBytes, err := encoding.EncodeMessagePackBytes(newCoupleAnalysis) if (err != nil) { return false, "", err } @@ -1136,144 +1119,187 @@ func GetOffspringPolygenicDiseaseInfo(diseaseLociList []polygenicDiseases.Diseas //Outputs: -// -bool: Any rules tested (if false, no offspring trait information is known) -// -int: Number of rules tested +// -bool: A neural network exists for this trait +// -bool: Analysis exists (at least 1 locus exists for this analysis from both people's genomes +// -map[string]int: Outcome probabilities map +// Map Structure: Outcome Name -> Offspring probability of outcome +// -int: Average prediction confidence (the average prediction confidence for all prospective offspring) +// -int: Quantity of loci tested +// -int: Quantity of parental phased loci +// -error +func GetOffspringDiscreteTraitInfo_NeuralNetwork(traitObject traits.Trait, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, bool, map[string]int, int, int, int, error){ + + traitName := traitObject.TraitName + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ + return false, false, nil, 0, 0, 0, errors.New("GetOffspringDiscreteTraitInfo_NeuralNetwork called with non-discrete trait.") + } + + modelExists, _ := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (modelExists == false){ + // Neural network prediction is not possible for this trait + return false, false, nil, 0, 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, nil, 0, 0, 0, err } + if (anyLocusValueExists == false){ + return true, false, nil, 0, 0, 0, nil + } + + // Map Structure: Outcome Name -> Probability of outcome coming true + // Because we are summing from 100 offspring, the count of outcomes is the same as the probability of an offspring having the outcome + outcomeCountsMap := make(map[string]int) + + // This is a sum of each prediction's confidence + predictionConfidencesSum := 0 + + quantityOfLociTested := 0 + + for index, offspringGenomeMap := range prospectiveOffspringGenomesList{ + + neuralNetworkExists, predictionIsKnown, predictedOutcome, predictionConfidence, currentQuantityOfLociTested, _, err := createPersonGeneticAnalysis.GetGenomeDiscreteTraitAnalysis_NeuralNetwork(traitObject, offspringGenomeMap, false) + if (err != nil){ return false, false, nil, 0, 0, 0, err } + if (neuralNetworkExists == false){ + return false, false, nil, 0, 0, 0, errors.New("GetGenomeTraitAnalysis_NeuralNetwork claiming that neural network doesn't exist when we already checked.") + } + if (predictionIsKnown == false){ + return false, false, nil, 0, 0, 0, errors.New("GetGenomeTraitAnalysis_NeuralNetwork claiming that prediction is impossible when we already know at least 1 locus value exists for trait.") + } + + outcomeCountsMap[predictedOutcome] += 1 + predictionConfidencesSum += predictionConfidence + + if (index == 0){ + // This value should be the same for each predicted offspring + quantityOfLociTested = currentQuantityOfLociTested + } + } + + averagePredictionConfidence := predictionConfidencesSum/100 + + return true, true, outcomeCountsMap, averagePredictionConfidence, quantityOfLociTested, quantityOfParentalPhasedLoci, nil +} + +//Outputs: +// -bool: Any rules exist (if false, rule-based prediction is not possible for this trait) +// -bool: Rule-based analysis exists (if false, no offspring trait information is known, or there is an outcome tie for one of the offspring) +// -int: Quantity of rules tested +// -int: Quantity of loci known // -map[[3]byte]int: Offspring probability of passing rules map // Map Structure: Rule identifier -> Offspring probability of passing rule (1-100) -// -map[string]float64: Offspring average outcome scores map -// Map Structure: Outcome Name -> Offspring average outcome score +// If a rule entry doesn't exist, we don't know the passes-rule probability for any of the offspring +// -map[string]int: Offspring outcome probabilities map +// Map Structure: Outcome Name -> Offspring probability of outcome (0-100) // -error -func GetOffspringTraitInfo(traitObject traits.Trait, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, int, map[[3]byte]int, map[string]float64, error){ +func GetOffspringDiscreteTraitInfo_Rules(traitObject traits.Trait, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, bool, int, int, map[[3]byte]int, map[string]int, error){ + + traitRulesList := traitObject.RulesList + + if (len(traitRulesList) == 0){ + return false, false, 0, 0, nil, nil, nil + } if (len(person1LocusValuesMap) == 0){ - return false, 0, nil, nil, nil + return true, false, 0, 0, nil, nil, nil } if (len(person2LocusValuesMap) == 0){ - return false, 0, nil, nil, nil + return true, false, 0, 0, nil, nil, nil } // First, we create 100 prospective offspring genomes. - traitLociList := traitObject.LociList + traitLociList_Rules := traitObject.LociList_Rules - anyLocusValueExists, prospectiveOffspringGenomesList, err := getProspectiveOffspringGenomesList(traitLociList, person1LocusValuesMap, person2LocusValuesMap) - if (err != nil) { return false, 0, nil, nil, err } + anyLocusValueExists, prospectiveOffspringGenomesList, err := getProspectiveOffspringGenomesList(traitLociList_Rules, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return false, false, 0, 0, nil, nil, err } if (anyLocusValueExists == false){ - return false, 0, nil, nil, nil + return true, false, 0, 0, nil, nil, nil } - traitRulesList := traitObject.RulesList - // Map Structure: Rule Identifier -> Number of offspring who pass the rule (out of 100 prospective offspring) + // Because there are 100 offspring, this also represents the percentage probability that an offspring will pass the rule offspringPassesRulesCountMap := make(map[[3]byte]int) - // We use this map to keep track of the rules for which we know every offspring's passes-rule status - // Map Structure: Rule Identifier -> Rule Object - offspringRulesWithKnownStatusMap := make(map[[3]byte]traits.TraitRule) + // This map stores the quantity of offspring who have each outcome + // The probability an offspring will have this outcome is the same as the + // quantity of offspring who have this outcome in our set of 100 randomly generated offspring + // Map structure: Outcome name -> quantity of offspring who have this outcome + outcomeCountsMap := make(map[string]int) - for offspringIndex, offspringGenomeMap := range prospectiveOffspringGenomesList{ + quantityOfLociKnown := 0 - // We iterate through rules to determine genome pair trait info + for index, offspringGenomeMap := range prospectiveOffspringGenomesList{ - for _, ruleObject := range traitRulesList{ + // Now we get outcome prediction for prospective offspring - ruleIdentifierHex := ruleObject.RuleIdentifier + anyRulesExist, quantityOfRulesTested, currentQuantityOfLociKnown, offspringPassesRulesMap, predictionOutcomeIsKnown, predictedOutcome, err := createPersonGeneticAnalysis.GetGenomeDiscreteTraitAnalysis_Rules(traitObject, offspringGenomeMap, false) + if (err != nil) { return false, false, 0, 0, nil, nil, err } + if (anyRulesExist == false){ + return false, false, 0, 0, nil, nil, errors.New("GetGenomeTraitAnalysis_Rules returning noRulesExists when we already checked and trait rules do in-fact exist.") + } + if (quantityOfRulesTested == 0){ + // This will be the same for each of the 100 generated offspring + // No analysis is possible. + return true, false, 0, currentQuantityOfLociKnown, nil, nil, nil + } + if (index == 0){ + // currentQuantityOfLociKnown will be the same for each prospective offspring + quantityOfLociKnown = currentQuantityOfLociKnown + } - ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) - if (err != nil) { return false, 0, nil, nil, err } - - if (offspringIndex == 0){ - - offspringRulesWithKnownStatusMap[ruleIdentifier] = ruleObject - } else { - - _, exists := offspringRulesWithKnownStatusMap[ruleIdentifier] - if (exists == false){ - // We already tried to check a previous offspring's passes-rule status for this rule - // We know that the offspring's passes-rule status will be unknown for every prospective offspring - continue - } - } - - // This is a list that describes the locus rsids and their values that must be fulfilled to pass the rule - ruleLocusObjectsList := ruleObject.LociList - - offspringPassesRuleIsKnown, offspringPassesRule, err := createPersonGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLocusObjectsList, offspringGenomeMap, false) - if (err != nil){ return false, 0, nil, nil, err } - if (offspringPassesRuleIsKnown == false){ - continue - } - - if (offspringPassesRule == true){ + for ruleIdentifier, genomePassesRule := range offspringPassesRulesMap{ + if (genomePassesRule == true){ offspringPassesRulesCountMap[ruleIdentifier] += 1 } } - } - // Map Structure: Rule Identifier -> Offspring Probability Of Passing Rule - // The map value stores the probability that the offspring will pass the rule - // This is a number between 0-100% - offspringProbabilityOfPassingRulesMap := make(map[[3]byte]int) - - // Map Structure: Outcome Name -> Outcome Score - // Example: "Intolerant" -> 2.5 - offspringAverageOutcomeScoresMap := make(map[string]float64) - - for ruleIdentifier, ruleObject := range offspringRulesWithKnownStatusMap{ - - //Output: - // -int: Offspring probability of passing rule (0-100%) - getOffspringPercentageProbabilityOfPassingRule := func()int{ - - numberOfOffspringWhoPassRule, exists := offspringPassesRulesCountMap[ruleIdentifier] - if (exists == false){ - // None of the offspring passed the rule - return 0 - } - - // There are 100 tested offspring - // Thus, the percentage of offspring who passed the rule is the same as the number of offspring who passed the rule - // The probability of the offspring passing the rule is the same as the percentage of offspring who passed the rule - - return numberOfOffspringWhoPassRule + if (predictionOutcomeIsKnown == false){ + // There was a tie between outcomes for this offspring + // We can't predict anything about this trait for this couple using rules + // This is why we need to create rules which make it unlikely for a tie between outcomes to occur. + return true, false, 0, 0, nil, nil, nil } - offspringPercentageProbabilityOfPassingRule := getOffspringPercentageProbabilityOfPassingRule() - - offspringProbabilityOfPassingRulesMap[ruleIdentifier] = offspringPercentageProbabilityOfPassingRule - - // This is the 0 - 1 probability value - offspringProbabilityOfPassingRule := float64(offspringPercentageProbabilityOfPassingRule)/100 - - ruleOutcomePointsMap := ruleObject.OutcomePointsMap - - for outcomeName, outcomePointsEffect := range ruleOutcomePointsMap{ - - pointsToAdd := float64(outcomePointsEffect) * offspringProbabilityOfPassingRule - - offspringAverageOutcomeScoresMap[outcomeName] += pointsToAdd - } + outcomeCountsMap[predictedOutcome] += 1 } - numberOfRulesTested := len(offspringProbabilityOfPassingRulesMap) + quantityOfRulesTested := len(offspringPassesRulesCountMap) - if (numberOfRulesTested == 0){ - return false, 0, nil, nil, nil - } - - traitOutcomesList := traitObject.OutcomesList - - // We add all outcomes for which there were no points - - for _, traitOutcome := range traitOutcomesList{ - - _, exists := offspringAverageOutcomeScoresMap[traitOutcome] - if (exists == false){ - offspringAverageOutcomeScoresMap[traitOutcome] = 0 - } - } - - return true, numberOfRulesTested, offspringProbabilityOfPassingRulesMap, offspringAverageOutcomeScoresMap, nil + return true, true, quantityOfRulesTested, quantityOfLociKnown, offspringPassesRulesCountMap, outcomeCountsMap, nil } @@ -1281,6 +1307,10 @@ func GetOffspringTraitInfo(traitObject traits.Trait, person1LocusValuesMap map[i // Each genome represents an equal-probability offspring genome from both people's genomes // This function takes into account the effects of genetic linkage // Any locations which do not exist in both people's genomes will not be included +// +// TODO: The user should be able to choose how many prospective offspring to create in the settings. +// More offspring will take longer, but will yield a more accurate trait analysis. +// //Outputs: // -bool: Any locus value exists between both users // -[]map[int64]locusValue.LocusValue @@ -1388,7 +1418,7 @@ func getProspectiveOffspringGenomesList(lociList []int64, person1LociMap map[int // We step by 1,000,000 each time // It would be more realistic if we did it in 1 integer increments, but it would be slower - for position := int64(0); position <= chromosomeLength; position += 1000000{ + for position := int64(0); position < chromosomeLength; position += 1_000_000{ //From Wikipedia: // A centimorgan (abbreviated cM) is a unit for measuring genetic linkage. @@ -1441,14 +1471,19 @@ func getProspectiveOffspringGenomesList(lociList []int64, person1LociMap map[int } personLocusBase1 := personLocusValue.Base1Value - personLocusBase2 := personLocusValue.Base1Value + personLocusBase2 := personLocusValue.Base2Value personLocusIsPhased := personLocusValue.LocusIsPhased + if (personLocusBase1 == personLocusBase2){ + // Phase doesn't matter + return true, personLocusBase1, nil + } + if (personLocusIsPhased == false){ // Breakpoints are unnecessary // We either choose base 1 or 2 - randomBool := helpers.GetRandomBool() - if (randomBool == true){ + randomInt := pseudorandomNumberGenerator.IntN(2) + if (randomInt == 1){ return true, personLocusBase1, nil } return true, personLocusBase2, nil @@ -1490,9 +1525,9 @@ func getProspectiveOffspringGenomesList(lociList []int64, person1LociMap map[int getLocusListIndex := func()int{ - for index, breakpoint := range personBreakpointsList{ + for index, breakpointPosition := range personBreakpointsList{ - if (int64(locusPosition) <= breakpoint){ + if (int64(locusPosition) <= breakpointPosition){ return index } diff --git a/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go b/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go index 9c30f8c..5d687e7 100644 --- a/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go +++ b/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go @@ -22,13 +22,14 @@ import "seekia/resources/geneticReferences/traits" import "seekia/internal/encoding" import "seekia/internal/genetics/geneticAnalysis" +import "seekia/internal/genetics/geneticPrediction" import "seekia/internal/genetics/locusValue" import "seekia/internal/genetics/prepareRawGenomes" import "seekia/internal/helpers" import "errors" import "slices" -import "maps" +import "reflect" //Outputs: @@ -51,10 +52,23 @@ func CreatePersonGeneticAnalysis(genomesList []prepareRawGenomes.RawGenomeWithMe genomesWithMetadataList, allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(genomesList, prepareRawGenomesUpdatePercentageCompleteFunction) if (err != nil) { return false, "", err } + // This map stores each genome's locus values + // Map Structure: Genome Identifier -> Genome locus values map (rsID -> Locus Value) + genomesMap := make(map[[16]byte]map[int64]locusValue.LocusValue) + + for _, genomeWithMetadata := range genomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadata.GenomeIdentifier + genomeMap := genomeWithMetadata.GenomeMap + + genomesMap[genomeIdentifier] = genomeMap + } + newGeneticAnalysisObject := geneticAnalysis.PersonAnalysis{ AnalysisVersion: 1, CombinedGenomesExist: multipleGenomesExist, AllRawGenomeIdentifiersList: allRawGenomeIdentifiersList, + GenomesMap: genomesMap, } if (multipleGenomesExist == true){ @@ -107,20 +121,30 @@ func CreatePersonGeneticAnalysis(genomesList []prepareRawGenomes.RawGenomeWithMe traitObjectsList, err := traits.GetTraitObjectsList() if (err != nil) { return false, "", err } - // Map Structure: Trait Name -> PersonTraitInfo - analysisTraitsMap := make(map[string]geneticAnalysis.PersonTraitInfo) + // This map will always contain an entry for each discrete trait + // Map Structure: Trait Name -> PersonDiscreteTraitInfo + analysisDiscreteTraitsMap := make(map[string]geneticAnalysis.PersonDiscreteTraitInfo) + + // This map will not contain entries for traits which this person's genome has no known loci + // Map Structure: Trait Name -> PersonNumericTraitInfo + analysisNumericTraitsMap := make(map[string]geneticAnalysis.PersonNumericTraitInfo) for _, traitObject := range traitObjectsList{ - personTraitAnalysisObject, err := GetPersonTraitAnalysis(genomesWithMetadataList, traitObject) - if (err != nil) { return false, "", err } - traitName := traitObject.TraitName + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric - analysisTraitsMap[traitName] = personTraitAnalysisObject + if (traitIsDiscreteOrNumeric == "Discrete"){ + + personTraitAnalysisObject, err := GetPersonDiscreteTraitAnalysis(genomesWithMetadataList, traitObject) + if (err != nil) { return false, "", err } + + analysisDiscreteTraitsMap[traitName] = personTraitAnalysisObject + } } - newGeneticAnalysisObject.TraitsMap = analysisTraitsMap + newGeneticAnalysisObject.DiscreteTraitsMap = analysisDiscreteTraitsMap + newGeneticAnalysisObject.NumericTraitsMap = analysisNumericTraitsMap analysisBytes, err := encoding.EncodeMessagePackBytes(newGeneticAnalysisObject) if (err != nil) { return false, "", err } @@ -505,9 +529,9 @@ func GetPersonMonogenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw diseaseAnalysisObject := geneticAnalysis.PersonGenomeMonogenicDiseaseInfo{ PersonHasDisease: personHasDisease, - NumberOfVariantsTested: numberOfVariantsTested, - NumberOfLociTested: numberOfLociTested, - NumberOfPhasedLoci: numberOfPhasedLoci, + QuantityOfVariantsTested: numberOfVariantsTested, + QuantityOfLociTested: numberOfLociTested, + QuantityOfPhasedLoci: numberOfPhasedLoci, ProbabilityOfPassingADiseaseVariant: percentageProbabilityPersonWillPassADiseaseVariant, VariantsInfoMap: variantsInfoMap, } @@ -737,16 +761,15 @@ func GetPersonPolygenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw genomeLocusValuesMap[locusRSID] = locusValueObject } - anyLociTested, personDiseaseRiskScore, genomeNumberOfLociTested, genomeLociInfoMap, err := GetPersonGenomePolygenicDiseaseInfo(diseaseLociList, genomeLocusValuesMap, false) + anyLociTested, personDiseaseRiskScore, genomeNumberOfLociTested, genomeLociInfoMap, err := GetPersonGenomePolygenicDiseaseInfo(diseaseLociList, genomeLocusValuesMap, true) if (err != nil) { return emptyDiseaseInfoObject, err } if (anyLociTested == false){ continue } newDiseaseInfoObject := geneticAnalysis.PersonGenomePolygenicDiseaseInfo{ - NumberOfLociTested: genomeNumberOfLociTested, + QuantityOfLociTested: genomeNumberOfLociTested, RiskScore: personDiseaseRiskScore, - LocusValuesMap: genomeLocusValuesMap, LociInfoMap: genomeLociInfoMap, } @@ -777,7 +800,7 @@ func GetPersonPolygenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw for _, personGenomeDiseaseInfoObject := range personPolygenicDiseaseInfoMap{ currentGenomeRiskScore := personGenomeDiseaseInfoObject.RiskScore - currentGenomeNumberOfLociTested := personGenomeDiseaseInfoObject.NumberOfLociTested + currentGenomeNumberOfLociTested := personGenomeDiseaseInfoObject.QuantityOfLociTested if (firstItemReached == false){ genomeRiskScore = currentGenomeRiskScore @@ -855,106 +878,68 @@ func GetPersonPolygenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw //Outputs: -// -geneticAnalysis.PersonTraitInfo: Trait analysis object +// -geneticAnalysis.PersonDiscreteTraitInfo: Trait analysis object // -error -func GetPersonTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.GenomeWithMetadata, traitObject traits.Trait)(geneticAnalysis.PersonTraitInfo, error){ +func GetPersonDiscreteTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.GenomeWithMetadata, traitObject traits.Trait)(geneticAnalysis.PersonDiscreteTraitInfo, error){ - // We use this when returning errors - emptyPersonTraitInfo := geneticAnalysis.PersonTraitInfo{} - - traitLociList := traitObject.LociList - traitRulesList := traitObject.RulesList - - // Map Structure: Genome Identifier -> PersonGenomeTraitInfo - newPersonTraitInfoMap := make(map[[16]byte]geneticAnalysis.PersonGenomeTraitInfo) + // Map Structure: Genome Identifier -> PersonGenomeDiscreteTraitInfo + newPersonTraitInfoMap := make(map[[16]byte]geneticAnalysis.PersonGenomeDiscreteTraitInfo) for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier genomeMap := genomeWithMetadataObject.GenomeMap - // This map contains the locus values for the genome - // If an locus's entry doesn't exist, its value is unknown - // Map Structure: Locus rsID -> Locus Value - genomeLocusValuesMap := make(map[int64]locusValue.LocusValue) + newPersonGenomeTraitInfo := geneticAnalysis.PersonGenomeDiscreteTraitInfo{} - for _, locusRSID := range traitLociList{ + neuralNetworkExists, neuralNetworkOutcomeIsKnown, predictedOutcome, predictionConfidence, quantityOfLociTested, quantityOfPhasedLoci, err := GetGenomeDiscreteTraitAnalysis_NeuralNetwork(traitObject, genomeMap, true) + if (err != nil) { return geneticAnalysis.PersonDiscreteTraitInfo{}, err } + if (neuralNetworkExists == true){ - locusBasePairKnown, _, _, _, locusValueObject, err := GetLocusValueFromGenomeMap(true, genomeMap, locusRSID) - if (err != nil) { return emptyPersonTraitInfo, err } - if (locusBasePairKnown == false){ - continue - } + newPersonGenomeTraitInfo.NeuralNetworkExists = true - genomeLocusValuesMap[locusRSID] = locusValueObject - } + if (neuralNetworkOutcomeIsKnown == true){ - // This map contains the trait outcome scores for the genome - // Map Structure: Outcome Name -> Score - // Example: "Intolerant" -> 5 - traitOutcomeScoresMap := make(map[string]int) + newPersonGenomeTraitInfo.NeuralNetworkAnalysisExists = true - // Map Structure: Rule Identifier -> Genome Passes rule (true if the genome passes the rule) - personPassesRulesMap := make(map[[3]byte]bool) + newPersonGenomeTraitInfo_NeuralNetwork := geneticAnalysis.PersonGenomeDiscreteTraitInfo_NeuralNetwork{ - if (len(traitRulesList) != 0){ - - // At least 1 rule exists for this trait - - for _, ruleObject := range traitRulesList{ - - ruleIdentifierHex := ruleObject.RuleIdentifier - - ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) - if (err != nil) { return emptyPersonTraitInfo, err } - - ruleLociList := ruleObject.LociList - - genomePassesRuleIsKnown, genomePassesRule, err := GetGenomePassesTraitRuleStatus(ruleLociList, genomeMap, false) - if (err != nil) { return emptyPersonTraitInfo, err } - if (genomePassesRuleIsKnown == false){ - continue + PredictedOutcome: predictedOutcome, + PredictionConfidence: predictionConfidence, + QuantityOfLociKnown: quantityOfLociTested, + QuantityOfPhasedLoci: quantityOfPhasedLoci, } - personPassesRulesMap[ruleIdentifier] = genomePassesRule + newPersonGenomeTraitInfo.NeuralNetworkAnalysis = newPersonGenomeTraitInfo_NeuralNetwork + } + } - // The rule has been passed by this genome - // We add the outcome points for the rule to the traitOutcomeScoresMap + anyRulesExist, quantityOfRulesTested, quantityOfLociKnown, genomePassesRulesMap, predictedOutcomeExists, predictedOutcome, err := GetGenomeDiscreteTraitAnalysis_Rules(traitObject, genomeMap, true) + if (err != nil) { return geneticAnalysis.PersonDiscreteTraitInfo{}, err } + if (anyRulesExist == true){ + newPersonGenomeTraitInfo.AnyRulesExist = true - ruleOutcomePointsMap := ruleObject.OutcomePointsMap + if (quantityOfRulesTested != 0){ - for traitOutcome, pointsChange := range ruleOutcomePointsMap{ + newPersonGenomeTraitInfo.RulesAnalysisExists = true - traitOutcomeScoresMap[traitOutcome] += pointsChange + newPersonGenomeTraitInfo_Rules := geneticAnalysis.PersonGenomeDiscreteTraitInfo_Rules{ + + GenomePassesRulesMap: genomePassesRulesMap, + PredictedOutcomeExists: predictedOutcomeExists, + PredictedOutcome: predictedOutcome, + QuantityOfRulesTested: quantityOfRulesTested, + QuantityOfLociKnown: quantityOfLociKnown, } + + newPersonGenomeTraitInfo.RulesAnalysis = newPersonGenomeTraitInfo_Rules } } - traitOutcomesList := traitObject.OutcomesList - - // We add all outcomes for which there were no points - - for _, traitOutcome := range traitOutcomesList{ - - _, exists := traitOutcomeScoresMap[traitOutcome] - if (exists == false){ - traitOutcomeScoresMap[traitOutcome] = 0 - } - } - - numberOfRulesTested := len(personPassesRulesMap) - - newPersonGenomeTraitInfo := geneticAnalysis.PersonGenomeTraitInfo{ - NumberOfRulesTested: numberOfRulesTested, - LocusValuesMap: genomeLocusValuesMap, - OutcomeScoresMap: traitOutcomeScoresMap, - GenomePassesRulesMap: personPassesRulesMap, - } - newPersonTraitInfoMap[genomeIdentifier] = newPersonGenomeTraitInfo } - newPersonTraitInfoObject := geneticAnalysis.PersonTraitInfo{ + newPersonTraitInfoObject := geneticAnalysis.PersonDiscreteTraitInfo{ TraitInfoMap: newPersonTraitInfoMap, } @@ -968,40 +953,20 @@ func GetPersonTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.Gen getConflictExistsBool := func()(bool, error){ - //TODO: Check for locus value conflicts once locus values are used in neural network prediction. - - if (len(traitRulesList) == 0){ - return false, nil - } - - // We check to see if the outcome scores are the same for all genomes - // We also check each rule result + // We check to see if the analysis results are the same for all genomes firstItemReached := false - - outcomeScoresMap := make(map[string]int) - passesRulesMap := make(map[[3]byte]bool) + personGenomeTraitInfoObject := geneticAnalysis.PersonGenomeDiscreteTraitInfo{} for _, genomeTraitInfoObject := range newPersonTraitInfoMap{ - currentGenomeOutcomeScoresMap := genomeTraitInfoObject.OutcomeScoresMap - currentGenomePassesRulesMap := genomeTraitInfoObject.GenomePassesRulesMap - if (firstItemReached == false){ - outcomeScoresMap = currentGenomeOutcomeScoresMap - passesRulesMap = currentGenomePassesRulesMap - firstItemReached = true + personGenomeTraitInfoObject = genomeTraitInfoObject continue } - areEqual := maps.Equal(currentGenomeOutcomeScoresMap, outcomeScoresMap) + areEqual := reflect.DeepEqual(personGenomeTraitInfoObject, genomeTraitInfoObject) if (areEqual == false){ - // A conflict exists - return true, nil - } - areEqual = maps.Equal(currentGenomePassesRulesMap, passesRulesMap) - if (areEqual == false){ - // A conflict exists return true, nil } } @@ -1010,7 +975,7 @@ func GetPersonTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.Gen } conflictExists, err := getConflictExistsBool() - if (err != nil) { return emptyPersonTraitInfo, err } + if (err != nil) { return geneticAnalysis.PersonDiscreteTraitInfo{}, err } newPersonTraitInfoObject.ConflictExists = conflictExists @@ -1046,12 +1011,205 @@ func GetGenomePolygenicDiseaseLocusRiskInfo(locusRiskWeightsMap map[string]int, return riskWeight, true, oddsRatio, nil } +// We use this to generate 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) +// -bool: Neural network outcome is known (at least 1 locus value is known which is needed for the neural network +// -string: The predicted outcome (Example: "Blue") +// -int: Probability (0-100) that the outcome from neural network is true (confidence) +// -int: Quantity of loci tested +// -int: Quantity of phased loci +// -error +func GetGenomeDiscreteTraitAnalysis_NeuralNetwork(traitObject traits.Trait, genomeMap map[int64]locusValue.LocusValue, checkForAliases bool)(bool, bool, string, int, 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, 0, 0, err } + + traitName := traitObject.TraitName + + neuralNetworkModelExists, traitPredictionIsPossible, predictedOutcome, predictionConfidence, quantityOfLociKnown, quantityOfPhasedLoci, err := geneticPrediction.GetNeuralNetworkTraitPredictionFromGenomeMap(traitName, genomeLocusValuesMap) + if (err != nil) { return false, false, "", 0, 0, 0, err } + if (neuralNetworkModelExists == false){ + return false, false, "", 0, 0, 0, nil + } + if (traitPredictionIsPossible == false){ + return true, false, "", 0, 0, 0, nil + } + + return true, true, predictedOutcome, predictionConfidence, quantityOfLociKnown, quantityOfPhasedLoci, nil +} + +//Outputs: +// -bool: Rule-based trait prediction is available +// -int: Quantity of trait rules tested +// -int: Quantity of loci known +// -map[[3]byte]bool: Passed rules map (Rule Identifier -> Genome passes rule) +// -bool: Rule-based prediction outcome is known (at least 1 rule has been tested and there is no outcome tie) +// -string: The predicted outcome (Example: "Tolerant") +// -error +func GetGenomeDiscreteTraitAnalysis_Rules(traitObject traits.Trait, genomeMap map[int64]locusValue.LocusValue, checkForAliases bool)(bool, int, int, map[[3]byte]bool, bool, string, error){ + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ + return false, 0, 0, nil, false, "", errors.New("GetGenomeDiscreteTraitAnalysis_Rules called with non-discrete trait.") + } + + traitRulesList := traitObject.RulesList + + if (len(traitRulesList) == 0){ + // We can't predict this trait using rules + // This means that neural network prediction is the only alternative potential way to predict this trait + return false, 0, 0, nil, false, "", nil + } + + // This map contains the trait outcome scores for the genome + // Map Structure: Outcome Name -> Score + // Example: "Intolerant" -> 5 + traitOutcomeScoresMap := make(map[string]int) + + // Map Structure: Rule Identifier -> Genome Passes rule (true if the genome passes the rule) + personPassesRulesMap := make(map[[3]byte]bool) + + // This map stores each known loci + // Multiple rules can use the same loci, so we need a map to avoid duplicates + // Map Structure: Locus RSID -> Locus is known + lociAreKnownMap := make(map[int64]bool) + + for _, ruleObject := range traitRulesList{ + + ruleIdentifierHex := ruleObject.RuleIdentifier + + ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) + if (err != nil) { return false, 0, 0, nil, false, "", err } + + ruleLociList := ruleObject.LociList + + for _, locusObject := range ruleLociList{ + + locusRSID := locusObject.LocusRSID + + _, exists := lociAreKnownMap[locusRSID] + if (exists == true){ + // We already know if this locus is known + continue + } + locusIsKnown, _, _, _, _, err := GetLocusValueFromGenomeMap(checkForAliases, genomeMap, locusRSID) + if (err != nil) { return false, 0, 0, nil, false, "", err } + + lociAreKnownMap[locusRSID] = locusIsKnown + } + + genomePassesRuleIsKnown, genomePassesRule, err := GetGenomePassesDiscreteTraitRuleStatus(ruleLociList, genomeMap, checkForAliases) + if (err != nil) { return false, 0, 0, nil, false, "", err } + if (genomePassesRuleIsKnown == false){ + continue + } + + personPassesRulesMap[ruleIdentifier] = genomePassesRule + + // The rule has been passed by this genome + // We add the outcome points for the rule to the traitOutcomeScoresMap + + ruleOutcomePointsMap := ruleObject.OutcomePointsMap + + for traitOutcome, pointsChange := range ruleOutcomePointsMap{ + + traitOutcomeScoresMap[traitOutcome] += pointsChange + } + } + + quantityOfLociKnown := 0 + + for _, locusIsKnown := range lociAreKnownMap{ + if (locusIsKnown == true){ + quantityOfLociKnown += 1 + } + } + + quantityOfRulesTested := len(personPassesRulesMap) + + if (quantityOfRulesTested == 0){ + return true, 0, quantityOfLociKnown, personPassesRulesMap, false, "", nil + } + + // -bool: Outcome is known (will be false if there is a tie + // -string: + // -error + getOutcome := func()(bool, string, error){ + + largestOutcome := "" + largestOutcomePoints := 0 + tieExists := false + + for outcomeName, outcomePoints := range traitOutcomeScoresMap{ + + if (outcomePoints < 1){ + // This should never happen, because outcomes points should only be increased by integers which are at least 1 or greater + return false, "", errors.New("traitOutcomeScoresMap contains outcomePoints < 1.") + } + + if (outcomePoints > largestOutcomePoints){ + largestOutcome = outcomeName + largestOutcomePoints = outcomePoints + tieExists = false + continue + + } else if (outcomePoints == largestOutcomePoints){ + tieExists = true + } + } + + if (tieExists == true){ + return false, "", nil + } + + return true, largestOutcome, nil + } + + outcomeIsKnown, outcomeName, err := getOutcome() + if (err != nil) { return false, 0, 0, nil, false, "", err } + if (outcomeIsKnown == false){ + return true, quantityOfRulesTested, quantityOfLociKnown, personPassesRulesMap, false, "", nil + } + + return true, quantityOfRulesTested, quantityOfLociKnown, personPassesRulesMap, true, outcomeName, nil +} + // This function checks to see if a genome will pass a trait rule // Outputs: // -bool: Genome passes trait rule status is known // -bool: Genome passes trait rule // -error -func GetGenomePassesTraitRuleStatus(ruleLociList []traits.RuleLocus, genomeMap map[int64]locusValue.LocusValue, checkForAliases bool)(bool, bool, error){ +func GetGenomePassesDiscreteTraitRuleStatus(ruleLociList []traits.RuleLocus, genomeMap map[int64]locusValue.LocusValue, checkForAliases bool)(bool, bool, error){ // We check to see if genome passes all rule loci // To pass a rule, all of the rule's loci must be passed by the provided genome diff --git a/internal/genetics/geneticAnalysis/geneticAnalysis.go b/internal/genetics/geneticAnalysis/geneticAnalysis.go index 1bfb17b..26f4969 100644 --- a/internal/genetics/geneticAnalysis/geneticAnalysis.go +++ b/internal/genetics/geneticAnalysis/geneticAnalysis.go @@ -22,14 +22,27 @@ type PersonAnalysis struct{ OnlyIncludeSharedGenomeIdentifier [16]byte OnlyExcludeConflictsGenomeIdentifier [16]byte + // This map stores each genome's locus values + // Only the loci that belong in the locusMetadata package are inside of this map + // This is necessary, otherwise genetic analyses would be too large by containing each analyzed raw genome. + // Map Structure: Genome Identifier -> Genome locus values map (rsID -> Locus Value) + GenomesMap map[[16]byte]map[int64]locusValue.LocusValue + // Map Structure: Disease Name -> PersonMonogenicDiseaseInfo MonogenicDiseasesMap map[string]PersonMonogenicDiseaseInfo // Map Structure: Disease Name -> PersonPolygenicDiseaseInfo PolygenicDiseasesMap map[string]PersonPolygenicDiseaseInfo + // These are traits which have discrete outcomes, rather than numeric outcomes + // For example: Eye color // Map Structure: Trait Name -> Trait Info Object - TraitsMap map[string]PersonTraitInfo + DiscreteTraitsMap map[string]PersonDiscreteTraitInfo + + // These are traits which have numeric outcomes, rather than discrete outcomes + // For example: Height + // Map Structure: Trait Name -> Trait Info Object + NumericTraitsMap map[string]PersonNumericTraitInfo } @@ -50,15 +63,15 @@ type PersonGenomeMonogenicDiseaseInfo struct{ PersonHasDisease bool // This describes the number of variants that were tested for this disease - NumberOfVariantsTested int + QuantityOfVariantsTested int // This describes the number of loci that were tested for this disease // 1 locus can have multiple potential variants - NumberOfLociTested int + QuantityOfLociTested int // This describes the number of loci which are phased - // This number will always be <= NumberOfLociTested - NumberOfPhasedLoci int + // This number will always be <= QuantityOfLociTested + QuantityOfPhasedLoci int // This describes the probability that the person will pass a disease variant // It is a value that represents a percentage between 0-100 @@ -94,14 +107,9 @@ type PersonPolygenicDiseaseInfo struct{ type PersonGenomePolygenicDiseaseInfo struct{ - // This describes the number of loci tested for this disease - // This should be len(LociInfoList) - NumberOfLociTested int - - // This map contains the locus values for the genome for this trait - // If an locus's entry doesn't exist, its value is unknown - // Map Structure: Locus rsID -> Locus Value - LocusValuesMap map[int64]locusValue.LocusValue + // This describes the quantity of loci tested for this disease + // This should be len(LociInfoMap) + QuantityOfLociTested int // This is total risk score for this disease for the person's genome // This is a number between 1-10 @@ -128,34 +136,102 @@ type PersonGenomePolygenicDiseaseLocusInfo struct{ } -type PersonTraitInfo struct{ +type PersonDiscreteTraitInfo struct{ // This map contains the person's trait info for each genome // If no map entries exist, then no trait info is known - // Map Structure: Genome Identifier -> PersonGenomeTraitInfo - TraitInfoMap map[[16]byte]PersonGenomeTraitInfo + // Map Structure: Genome Identifier -> PersonGenomeDiscreteTraitInfo + TraitInfoMap map[[16]byte]PersonGenomeDiscreteTraitInfo // This is true if there are multiple genomes and the results from each genome differ ConflictExists bool } -type PersonGenomeTraitInfo struct{ +// For a trait analysis, both analysis methods may exist in the results +// However, the GUI will only display the results from one of the methods. +// The neural network prediction is always prioritized over the rule-based prediction +type PersonGenomeDiscreteTraitInfo struct{ - // This should be len(GenomePassesRulesMap) - NumberOfRulesTested int + // This is true if it is possible to analyze this trait using a neural network + NeuralNetworkExists bool - // This map contains the locus values for the genome for this trait - // If an locus's entry doesn't exist, its value is unknown - // Map Structure: Locus rsID -> Locus Value - LocusValuesMap map[int64]locusValue.LocusValue + // This is true if a neural network analysis was performed for this genome + // This means that at least 1 locus for this trait was contained in the genome + NeuralNetworkAnalysisExists bool - // This map contains the outcome scores for the genome - // Map Structure: Outcome Name -> Score - // Example: "Intolerant" -> 5 - OutcomeScoresMap map[string]int + NeuralNetworkAnalysis PersonGenomeDiscreteTraitInfo_NeuralNetwork + + // This is true if it is possible to analyze this trait using rules + AnyRulesExist bool + + // This is true if a rules-based analysis was performed for this genome + // This means that all of the loci for at least 1 rule for this trait was contained in the genome + RulesAnalysisExists bool + + RulesAnalysis PersonGenomeDiscreteTraitInfo_Rules +} + +type PersonGenomeDiscreteTraitInfo_NeuralNetwork struct{ + + // The predicted outcome (Example: "Blue") + PredictedOutcome string + + // Probability (0-100) that the outcome from the neural network is true + PredictionConfidence int + + QuantityOfLociKnown int + + QuantityOfPhasedLoci int +} + +type PersonGenomeDiscreteTraitInfo_Rules struct{ // Map Structure: Rule Identifier -> Genome Passes rule (true if the genome passes the rule) GenomePassesRulesMap map[[3]byte]bool + + // This is true if there was not a tie between summed rule outcome scores + // It is possible to have some tested rules without a known outcome + PredictedOutcomeExists bool + + // This is the outcome that was predicted + // Example: "Intolerant" + PredictedOutcome string + + // This should be len(GenomePassesRulesMap) + QuantityOfRulesTested int + + // This only counts the loci which are used for rules + // For example, loci that are only used in neural-network-based prediction are not counted + QuantityOfLociKnown int +} + + +type PersonNumericTraitInfo struct{ + + // This map contains the person's trait info for each genome + // If no map entries exist, then no trait info is known + // Map Structure: Genome Identifier -> PersonGenomeNumericTraitInfo + TraitInfoMap map[[16]byte]PersonGenomeNumericTraitInfo + + // This is true if there are multiple genomes and the results from each genome differ + ConflictExists bool +} + +type PersonGenomeNumericTraitInfo struct{ + + // The predicted outcome (Example: The predicted height for this person, in centimeters) + PredictedOutcome float64 + + // This map stores the confidence ranges for the predicted value + // If we want to know how accurate the prediction is with a X% accuracy, how far would we have to expand the + // 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 + ConfidenceRangesMap map[int]float64 + + QuantityOfLociKnown int + + QuantityOfPhasedLoci int } @@ -190,16 +266,21 @@ type CoupleAnalysis struct{ // Map Structure: Disease Name -> OffspringPolygenicDiseaseInfo PolygenicDiseasesMap map[string]OffspringPolygenicDiseaseInfo + // Discrete traits are traits with discrete outcomes, such as Eye Color // Map Structure: Trait Name -> Trait Info Object - TraitsMap map[string]OffspringTraitInfo + DiscreteTraitsMap map[string]OffspringDiscreteTraitInfo + + // Numeric traits are traits with numeric outcomes, such as Height + // Map Structure: Trait Name -> Trait Info Object + NumericTraitsMap map[string]OffspringNumericTraitInfo } type OffspringMonogenicDiseaseInfo struct{ - // This map stores the number of variants tested in each person's genome + // This map stores the quantity of variants tested in each person's genome // Map Structure: Genome Identifier -> Number of variants tested - NumberOfVariantsTestedMap map[[16]byte]int + QuantityOfVariantsTestedMap map[[16]byte]int // This map stores the offspring disease probabilities for each genome pair. // A genome pair is a concatenation of two genome identifiers @@ -215,7 +296,6 @@ type OffspringMonogenicDiseaseInfo struct{ type OffspringGenomePairMonogenicDiseaseInfo struct{ // At least 1 variant's information is needed from either person to include the diseaseInfo object in the MonogenicDiseaseInfoMap - ProbabilityOffspringHasDiseaseIsKnown bool // This is the probability that the offspring will have the disease @@ -259,8 +339,8 @@ type OffspringPolygenicDiseaseInfo struct{ type OffspringGenomePairPolygenicDiseaseInfo struct{ - // This should be len(DiseaseLociList) - NumberOfLociTested int + // This should be len(LociInfoMap) + QuantityOfLociTested int // A number between 1-10 representing the offspring's average risk score // 1 == lowest risk, 10 == highest risk @@ -297,27 +377,110 @@ type OffspringPolygenicDiseaseLocusInfo struct{ } -type OffspringTraitInfo struct{ +type OffspringDiscreteTraitInfo struct{ // This map stores the trait info for each genome pair // Map Structure: Genome Pair Identifier -> OffspringGenomePairTraitInfo - TraitInfoMap map[[32]byte]OffspringGenomePairTraitInfo + TraitInfoMap map[[32]byte]OffspringGenomePairDiscreteTraitInfo ConflictExists bool } -type OffspringGenomePairTraitInfo struct{ +// For a trait analysis, both analysis methods may exist in the results +// However, the GUI will only display the results from one of the methods. +// The neural network prediction is always prioritized over the rule-based prediction +type OffspringGenomePairDiscreteTraitInfo struct{ - // This should be len(TraitRulesList) - NumberOfRulesTested int + // This is true if it is possible to analyze this trait using a neural network + NeuralNetworkExists bool - // Map Structure: Outcome Name -> Outcome Score - // Example: "Intolerant" -> 2.5 - OffspringAverageOutcomeScoresMap map[string]float64 + // This is true if a neural network analysis was performed for this genome + // This means that at least 1 locus for this trait was contained in both of the genomes in the pair + NeuralNetworkAnalysisExists bool + + NeuralNetworkAnalysis OffspringGenomePairDiscreteTraitInfo_NeuralNetwork + + // This is true if it is possible to analyze this trait using rules + RulesExist bool + + // This is true if a rules-based analysis was performed for this genome + // This means that all of the loci for at least 1 rule for this trait was contained in both of the genomes in the pair + // Also, none of the offspring have an unknown outcome caused by an outcome score tie + RulesAnalysisExists bool + + RulesAnalysis OffspringGenomePairDiscreteTraitInfo_Rules +} + + +type OffspringGenomePairDiscreteTraitInfo_NeuralNetwork struct{ + + // Map Structure: Outcome Name -> Outcome Probability (0-100) + // Example: "Intolerant" -> 5 + OffspringOutcomeProbabilitiesMap map[string]int + + // Probability (0-100) that each outcome from the neural network is true + // This is an average of the confidence for each of the calculated 100 outcome probabilities + AverageConfidence int + + 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 +} + +type OffspringGenomePairDiscreteTraitInfo_Rules struct{ + + // Map Structure: Outcome Name -> Outcome Probability (0-100) + // Example: "Intolerant" -> 5 + OffspringOutcomeProbabilitiesMap map[string]int // Map Structure: Rule Identifier -> Offspring Probability Of Passing Rule // The value stores the probability that the offspring will pass the rule // This is a number between 0-100% ProbabilityOfPassingRulesMap map[[3]byte]int + + // This should be len(ProbabilityOfPassingRulesMap) + QuantityOfRulesTested int + + // This only counts the loci which are used for rules + // For example, loci that are only used in neural-network-based prediction are not counted + QuantityOfLociKnown int } +type OffspringNumericTraitInfo struct{ + + // This map stores the trait info for each genome pair + // Map Structure: Genome Pair Identifier -> OffspringGenomePairNumericTraitInfo + TraitInfoMap map[[32]byte]OffspringGenomePairNumericTraitInfo + + ConflictExists bool +} + +type OffspringGenomePairNumericTraitInfo struct{ + + // The average outcome for the offspring + // For example, the average height for an offspring between these 2 people + OffspringAverageOutcome float64 + + // This map stores the confidence ranges for the predicted value + // If we want to know how accurate the prediction is with a X% accuracy, how far would we have to expand the + // 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 + + // 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 d167762..cf8b1d8 100644 --- a/internal/genetics/geneticPrediction/geneticPrediction.go +++ b/internal/genetics/geneticPrediction/geneticPrediction.go @@ -11,6 +11,7 @@ package geneticPrediction // We could create slower models that provide more accurate predictions import "seekia/resources/geneticReferences/traits" +import "seekia/resources/geneticPredictionModels" import "seekia/internal/genetics/locusValue" import "seekia/internal/genetics/readBiobankData" @@ -211,6 +212,252 @@ func DecodeBytesToNeuralNetworkObject(inputNeuralNetwork []byte)(NeuralNetwork, return newNeuralNetworkObject, nil } +// This map is used to store information about how accurate genetic prediction models are +// Map Structure: Trait Outcome Info -> Trait Prediction Accuracy Info +type TraitPredictionAccuracyInfoMap map[TraitOutcomeInfo]TraitPredictionAccuracyInfo + +type TraitOutcomeInfo struct{ + + // This is the outcome which was found + // Example: "Blue" + OutcomeName string + + // This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction + PercentageOfLociTested int + + // This is a value between 0-100 which describes the percentage of the tested loci which were phased for the input for the prediction + PercentageOfPhasedLoci int +} + +type TraitPredictionAccuracyInfo struct{ + + // This contains the quantity of examples for the outcome with the specified percentageOfLociTested and percentageOfPhasedLoci + QuantityOfExamples int + + // This contains the quantity of predictions for the outcome with the specified percentageOfLociTested and percentageOfPhasedLoci + // Prediction = our model predicted this outcome + QuantityOfPredictions int + + // This stores the probability (0-100) that our model will accurately predict this outcome for a genome which has + // the specified percentageOfLociTested and percentageOfPhasedLoci + // In other words: What is the probability that if you give Seekia a blue-eyed genome, it will give you a correct Blue prediction? + // This value is only accurate is QuantityOfExamples > 0 + ProbabilityOfCorrectGenomePrediction int + + // This stores the probability (0-100) that our model is correct if our model predicts that a genome + // with the specified percentageOfLociTested and percentageOfPhasedLoci has this outcome + // In other words: What is the probability that if Seekia says a genome will have blue eyes, it is correct? + // This value is only accurate is QuantityOfPredictions > 0 + ProbabilityOfCorrectOutcomePrediction int +} + +func EncodeTraitPredictionAccuracyInfoMapToBytes(inputMap TraitPredictionAccuracyInfoMap)([]byte, error){ + + buffer := new(bytes.Buffer) + + encoder := gob.NewEncoder(buffer) + + err := encoder.Encode(inputMap) + if (err != nil) { return nil, err } + + inputMapBytes := buffer.Bytes() + + return inputMapBytes, nil +} + +func DecodeBytesToTraitPredictionAccuracyInfoMap(inputBytes []byte)(TraitPredictionAccuracyInfoMap, error){ + + if (inputBytes == nil){ + return nil, errors.New("DecodeBytesToTraitPredictionAccuracyInfoMap called with nil inputBytes.") + } + + buffer := bytes.NewBuffer(inputBytes) + + decoder := gob.NewDecoder(buffer) + + var newTraitPredictionAccuracyInfoMap TraitPredictionAccuracyInfoMap + + err := decoder.Decode(&newTraitPredictionAccuracyInfoMap) + if (err != nil){ return nil, err } + + return newTraitPredictionAccuracyInfoMap, 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) +// -string: Predicted trait outcome (Example: "Blue") +// -int: Confidence: Probability (0-100) that the prediction is accurate +// -int: Quantity of loci known +// -int: Quantity of phased loci +// -error +func GetNeuralNetworkTraitPredictionFromGenomeMap(traitName string, genomeMap map[int64]locusValue.LocusValue)(bool, bool, string, int, int, int, error){ + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, false, "", 0, 0, 0, err } + + // This is a map of rsIDs which influence this trait + traitRSIDsList := traitObject.LociList + + if (len(traitRSIDsList) == 0){ + // Neural network trait prediction is not possible for this trait + return false, false, "", 0, 0, 0, nil + } + + predictionModelExists, predictionModelBytes := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (predictionModelExists == false){ + // Neural network trait prediction is not possible for this trait + return false, false, "", 0, 0, 0, nil + } + + 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) + } + + if (quantityOfLociKnown == 0){ + // We can't predict anything about this trait for this genome + return true, false, "", 0, 0, 0, nil + } + + neuralNetworkObject, err := DecodeBytesToNeuralNetworkObject(predictionModelBytes) + if (err != nil) { return false, false, "", 0, 0, 0, err } + + outputLayer, err := GetNeuralNetworkRawPrediction(&neuralNetworkObject, neuralNetworkInput) + if (err != nil) { return false, false, "", 0, 0, 0, err } + + predictedOutcomeName, err := GetOutcomeNameFromOutputLayer(traitName, false, outputLayer) + if (err != nil) { return false, false, "", 0, 0, 0, err } + + modelTraitAccuracyInfoFile, err := geneticPredictionModels.GetPredictionModelTraitAccuracyInfoBytes(traitName) + if (err != nil) { return false, false, "", 0, 0, 0, err } + + modelTraitAccuracyInfoMap, err := DecodeBytesToTraitPredictionAccuracyInfoMap(modelTraitAccuracyInfoFile) + if (err != nil) { return false, false, "", 0, 0, 0, err } + + // We find the model trait accuracy info object that is the most similar to our predicted outcome + + getPredictionAccuracy := func()int{ + + 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 likely accuracy probability for this prediction + closestPredictionAccuracy := 0 + + // This is a value that represents the distance our closest prediction accuracy has from the current prediction + // Consider each prediction accuracy value on an (X,Y) coordinate plane + // X = Number of loci tested + // Y = Number of phased loci + closestPredictionAccuracyDistance := float64(0) + + anyOutcomeAccuracyFound := false + + for traitOutcomeInfo, traitPredictionAccuracyInfo := range modelTraitAccuracyInfoMap{ + + outcomeName := traitOutcomeInfo.OutcomeName + if (outcomeName != predictedOutcomeName){ + continue + } + + probabilityOfCorrectOutcomePrediction := traitPredictionAccuracyInfo.ProbabilityOfCorrectOutcomePrediction + + 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 accuracy + return probabilityOfCorrectOutcomePrediction + } + + if (anyOutcomeAccuracyFound == false){ + closestPredictionAccuracyDistance = distance + closestPredictionAccuracy = probabilityOfCorrectOutcomePrediction + anyOutcomeAccuracyFound = true + continue + } else { + if (distance < closestPredictionAccuracyDistance){ + closestPredictionAccuracyDistance = distance + closestPredictionAccuracy = probabilityOfCorrectOutcomePrediction + } + } + } + + if (anyOutcomeAccuracyFound == false){ + // This means that our model has never actually predicted this outcome + // This shouldn't happen unless our model is really bad, or our training set has very few people with this outcome. + // We return a 0% accuracy rating + return 0 + } + + return closestPredictionAccuracy + } + + predictionAccuracy := getPredictionAccuracy() + + return true, true, predictedOutcomeName, predictionAccuracy, quantityOfLociKnown, quantityOfPhasedLoci, nil +} + //Outputs: // -int: Number of loci values that are known // -int: Number of loci values that are known and phased @@ -437,9 +684,9 @@ func CreateGeneticPredictionTrainingData_OpenSNP( if (err != nil) { return false, nil, err } // This is a list of rsIDs which influence this trait - traitRSIDs := traitObject.LociList + traitRSIDsList := traitObject.LociList - if (len(traitRSIDs) == 0){ + if (len(traitRSIDsList) == 0){ return false, nil, errors.New("traitObject contains no rsIDs.") } @@ -457,7 +704,7 @@ func CreateGeneticPredictionTrainingData_OpenSNP( // -0 = Locus value is unknown // -0.5 = Locus Is known, phase is unknown // -1 = Locus Is Known, phase is known - expectedNumberOfInputLayerRows := len(traitRSIDs) * 3 + expectedNumberOfInputLayerRows := len(traitRSIDsList) * 3 if (numberOfInputLayerRows != expectedNumberOfInputLayerRows){ @@ -468,7 +715,7 @@ func CreateGeneticPredictionTrainingData_OpenSNP( checkIfAnyTraitLocusValuesExist := func()bool{ - for _, rsID := range traitRSIDs{ + for _, rsID := range traitRSIDsList{ _, exists := userLocusValuesMap[rsID] if (exists == true){ @@ -487,11 +734,9 @@ func CreateGeneticPredictionTrainingData_OpenSNP( } // We sort rsIDs in ascending order - // We copy list so we don't change the original - traitRSIDsList := slices.Clone(traitRSIDs) - - slices.Sort(traitRSIDsList) + traitRSIDsListCopy := slices.Clone(traitRSIDsList) + slices.Sort(traitRSIDsListCopy) // This function returns the outputLayer for all trainingDatas for this user // Each outputLayer represents the user's trait value (Example: "Blue" for Eye Color) @@ -637,11 +882,11 @@ func CreateGeneticPredictionTrainingData_OpenSNP( anyLocusExists := false - inputLayerLength := len(traitRSIDsList) * 3 + inputLayerLength := len(traitRSIDsListCopy) * 3 inputLayer := make([]float32, 0, inputLayerLength) - for _, rsID := range traitRSIDsList{ + for _, rsID := range traitRSIDsListCopy{ randomFloat := pseudorandomNumberGenerator.Float64() if (randomFloat > probabilityOfUsingLoci){ @@ -996,7 +1241,7 @@ func (inputNetwork *NeuralNetwork)buildNeuralNetwork(inputLayer *gorgonia.Node)e inputLayerCopy := inputLayer - // We multiply weights at each layer and perform sigmoid after each multiplication + // We multiply weights at each layer and perform ReLU (Rectification) after each multiplication weights1 := inputNetwork.weights1 diff --git a/internal/genetics/myChosenAnalysis/myChosenAnalysis.go b/internal/genetics/myChosenAnalysis/myChosenAnalysis.go index a71e7dc..a798f9a 100644 --- a/internal/genetics/myChosenAnalysis/myChosenAnalysis.go +++ b/internal/genetics/myChosenAnalysis/myChosenAnalysis.go @@ -27,7 +27,6 @@ var myCacheChosenGeneticAnalysisIdentifier string // We use this variable to store the analysis in memory // This prevents us from having to read and unmarshal the messagepack file each time we want to retrieve the analysis -// TODO: Read attributes from the analysis into maps for faster retrieval var myCacheChosenGeneticAnalysis geneticAnalysis.PersonAnalysis // These variables store metadata about the cache genetic analysis @@ -36,6 +35,7 @@ var myCacheChosenGeneticAnalysis_GenomeIdentifierToUse [16]byte // This variable tells us if the current cache chosen genetic analysis contains multiple genomes var myCacheChosenGeneticAnalysis_MultipleGenomesExist bool + // This function is used to retrieve the user's chosen person genetic analysis // The user must choose a person to link to their mate profile/identity // This genetic analysis is not shared on the person's profile publicly. @@ -101,7 +101,7 @@ func GetMyChosenMateGeneticAnalysis()(bool, bool, bool, geneticAnalysis.PersonAn return false, false, false, emptyPersonAnalysis, [16]byte{}, false, errors.New("CheckIfPersonAnalysisIsReady returning missing genetic analysis.") } - allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject) + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject) if (err != nil) { return false, false, false, emptyPersonAnalysis, [16]byte{}, false, err } getGenomeIdentifierToUse := func()([16]byte, error){ diff --git a/internal/genetics/myGenomes/myGenomes.go b/internal/genetics/myGenomes/myGenomes.go index 1b77a2a..684ee61 100644 --- a/internal/genetics/myGenomes/myGenomes.go +++ b/internal/genetics/myGenomes/myGenomes.go @@ -6,6 +6,7 @@ package myGenomes import "seekia/internal/cryptography/blake3" import "seekia/internal/encoding" +import "seekia/internal/genetics/prepareRawGenomes" import "seekia/internal/genetics/readRawGenomes" import "seekia/internal/helpers" import "seekia/internal/localFilesystem" @@ -106,11 +107,18 @@ func AddRawGenome(personIdentifier string, rawGenomeString string)(bool, bool, e rawGenomeReader := strings.NewReader(rawGenomeString) - companyName, importVersion, timeFileWasGenerated, snpCount, genomeIsPhased, _, err := readRawGenomes.ReadRawGenomeFile(rawGenomeReader) + companyName, importVersion, timeFileWasGenerated, snpCount, genomeIsPhased, rawGenomeMap, err := readRawGenomes.ReadRawGenomeFile(rawGenomeReader) if (err != nil){ return false, false, nil } + genomeHasUsefulLocations, _, err := prepareRawGenomes.ConvertRawGenomeToGenomeMap(rawGenomeMap, genomeIsPhased) + if (err != nil) { return false, false, err } + if (genomeHasUsefulLocations == false){ + //TODO: Explain this to the user rather than just telling the user that the file is invalid + return false, false, nil + } + genomeIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { return false, false, err } diff --git a/internal/genetics/prepareRawGenomes/prepareRawGenomes.go b/internal/genetics/prepareRawGenomes/prepareRawGenomes.go index 8aa5fbf..7ec0a32 100644 --- a/internal/genetics/prepareRawGenomes/prepareRawGenomes.go +++ b/internal/genetics/prepareRawGenomes/prepareRawGenomes.go @@ -76,6 +76,10 @@ func CreateRawGenomeWithMetadataObject(genomeIdentifier [16]byte, rawGenomeStrin // -error func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWithMetadata, updatePercentageCompleteFunction func(int)error)([]GenomeWithMetadata, [][16]byte, bool, [16]byte, [16]byte, error){ + if (len(inputGenomesList) == 0){ + return nil, nil, false, [16]byte{}, [16]byte{}, errors.New("GetGenomesWithMetadataListFromRawGenomesList called with empty inputGenomesList") + } + // The reading of genomes will take up the first 20% of the percentage range // The creation of multiple genomes will take up the last 80% of the percentage range @@ -102,103 +106,13 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi // Now we convert rawGenomeMap to a genomeMap - // Map Structure: RSID -> Locus Value - genomeMap := make(map[int64]locusValue.LocusValue) - - // We use this list to check for alias collisions later - allRSIDsList := make([]int64, 0) - - for rsID, locusBasePairValue := range rawGenomeMap{ - - locusAllele2Exists := locusBasePairValue.Allele2Exists - if (locusAllele2Exists == false){ - // This SNP contains less than 2 bases - // We don't support reading these kinds of SNP values yet - continue - } - - locusAllele1 := locusBasePairValue.Allele1 - locusAllele2 := locusBasePairValue.Allele2 - - getLocusIsPhasedBool := func()bool{ - - if (locusAllele1 == locusAllele2){ - // Locus has to be phased, because phase flip does not change value - return true - } - - return genomeIsPhased - } - - locusIsPhased := getLocusIsPhasedBool() - - locusValueObject := locusValue.LocusValue{ - LocusIsPhased: locusIsPhased, - Base1Value: locusAllele1, - Base2Value: locusAllele2, - } - - genomeMap[rsID] = locusValueObject - - allRSIDsList = append(allRSIDsList, rsID) - } - - // Now we check for rsID aliases - // rsID aliases are multiple rsids that refer to the same location - // If a single genome file contained two identical rsids whose value conflicted, the company must have made an error in reporting - - for _, rsID := range allRSIDsList{ - - rsidLocusValue, exists := genomeMap[rsID] - if (exists == false){ - // This must have been an alias that was deleted - continue - } - - aliasExists, rsidAliasesList, err := locusMetadata.GetRSIDAliases(rsID) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } - if (aliasExists == false){ - continue - } - - checkIfRSIDCollisionExists := func()bool{ - - for _, rsidAlias := range rsidAliasesList{ - - aliasLocusValue, exists := genomeMap[rsidAlias] - if (exists == false){ - continue - } - - if (aliasLocusValue != rsidLocusValue){ - // A collision exists with an alias rsID - // The company must be creating invalid results, or our alias list is invalid - return true - } - } - - return false - } - - rsidCollisionExists := checkIfRSIDCollisionExists() - if (rsidCollisionExists == true){ - // We will delete this rsID - // We cannot trust any of the results - - delete(genomeMap, rsID) - } - - // We delete all aliases from the genome map - // We do this to save space - - for _, rsidAlias := range rsidAliasesList{ - delete(genomeMap, rsidAlias) - } - } - - if (len(genomeMap) == 0){ - // No valid locations exist - continue + anyValuesExist, genomeMap, err := ConvertRawGenomeToGenomeMap(rawGenomeMap, genomeIsPhased) + if (err != nil) { return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (anyValuesExist == false){ + // We have to make sure this never happens so the user isn't confused as to why genomes + // that were imported were not included in the analysis + // We make sure this doesn't happen by verifying the genome at the time of importing + return nil, nil, false, [16]byte{}, [16]byte{}, errors.New("Genome supplied to GetGenomesWithMetadataListFromRawGenomesList has no valid locations.") } genomeWithMetadataObject := GenomeWithMetadata{ @@ -221,9 +135,9 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi err := updatePercentageCompleteFunction(20) if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } - if (len(genomesWithMetadataList) == 1){ + if (len(genomesWithMetadataList) <= 1){ - // Only 1 genome exists. + // <=1 genome exists. // No genome combining is needed. err = updatePercentageCompleteFunction(100) @@ -547,4 +461,112 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi return genomesWithMetadataList, allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, nil } +//Outputs: +// -bool: Genome has any useful locations +// -map[int64]locusValue.LocusValue +// -error +func ConvertRawGenomeToGenomeMap(rawGenomeMap map[int64]readRawGenomes.RawGenomeLocusValue, genomeIsPhased bool)(bool, map[int64]locusValue.LocusValue, error){ + + // Map Structure: RSID -> Locus Value + genomeMap := make(map[int64]locusValue.LocusValue) + + // We use this list to check for alias collisions later + allRSIDsList := make([]int64, 0) + + for rsID, locusBasePairValue := range rawGenomeMap{ + + locusAllele2Exists := locusBasePairValue.Allele2Exists + if (locusAllele2Exists == false){ + // This SNP contains less than 2 bases + // We don't support reading these kinds of SNP values yet + continue + } + + locusAllele1 := locusBasePairValue.Allele1 + locusAllele2 := locusBasePairValue.Allele2 + + getLocusIsPhasedBool := func()bool{ + + if (locusAllele1 == locusAllele2){ + // Locus has to be phased, because phase flip does not change value + return true + } + + return genomeIsPhased + } + + locusIsPhased := getLocusIsPhasedBool() + + locusValueObject := locusValue.LocusValue{ + Base1Value: locusAllele1, + Base2Value: locusAllele2, + LocusIsPhased: locusIsPhased, + } + + genomeMap[rsID] = locusValueObject + + allRSIDsList = append(allRSIDsList, rsID) + } + + // Now we check for rsID aliases + // rsID aliases are multiple rsids that refer to the same location + // If a single genome file contained two identical rsids whose value conflicted, the company must have made an error in reporting + + for _, rsID := range allRSIDsList{ + + rsidLocusValue, exists := genomeMap[rsID] + if (exists == false){ + // This must have been an alias that was deleted + continue + } + + aliasExists, rsidAliasesList, err := locusMetadata.GetRSIDAliases(rsID) + if (err != nil){ return false, nil, err } + if (aliasExists == false){ + continue + } + + checkIfRSIDCollisionExists := func()bool{ + + for _, rsidAlias := range rsidAliasesList{ + + aliasLocusValue, exists := genomeMap[rsidAlias] + if (exists == false){ + continue + } + + if (aliasLocusValue != rsidLocusValue){ + // A collision exists with an alias rsID + // The company must be creating invalid results, or our alias list is invalid + return true + } + } + + return false + } + + rsidCollisionExists := checkIfRSIDCollisionExists() + if (rsidCollisionExists == true){ + // We will delete this rsID + // We cannot trust any of the results + + delete(genomeMap, rsID) + } + + // We delete all aliases from the genome map + // We do this to save space + + for _, rsidAlias := range rsidAliasesList{ + + delete(genomeMap, rsidAlias) + } + } + + if (len(genomeMap) == 0){ + // No valid locations exist + return false, nil, nil + } + + return true, genomeMap, nil +} diff --git a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go index df47362..a2ddffa 100644 --- a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go +++ b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go @@ -47,27 +47,30 @@ func ReadCoupleGeneticAnalysisString(inputAnalysisString string)(geneticAnalysis // -bool: Multiple genomes exist // -[16]byte: OnlyExcludeConflicts GenomeIdentifier // -[16]byte: OnlyIncludeShared GenomeIdentifier +// -map[[16]byte]map[int64]locusValue.LocusValue: Genomes locus values map // -error -func GetMetadataFromPersonGeneticAnalysis(inputGeneticAnalysis geneticAnalysis.PersonAnalysis)([][16]byte, bool, [16]byte, [16]byte, error){ +func GetMetadataFromPersonGeneticAnalysis(inputGeneticAnalysis geneticAnalysis.PersonAnalysis)([][16]byte, bool, [16]byte, [16]byte, map[[16]byte]map[int64]locusValue.LocusValue, error){ analysisVersion := inputGeneticAnalysis.AnalysisVersion if (analysisVersion != 1){ // This analysis must have been created by a newer version of Seekia // We cannot read it - return nil, false, [16]byte{}, [16]byte{}, errors.New("Cannot read analysis: Is a newer analysis version.") + return nil, false, [16]byte{}, [16]byte{}, nil, errors.New("Cannot read analysis: Is a newer analysis version.") } allRawGenomeIdentifiersList := inputGeneticAnalysis.AllRawGenomeIdentifiersList + genomesMap := inputGeneticAnalysis.GenomesMap + combinedGenomesExist := inputGeneticAnalysis.CombinedGenomesExist if (combinedGenomesExist == false){ - return allRawGenomeIdentifiersList, false, [16]byte{}, [16]byte{}, nil + return allRawGenomeIdentifiersList, false, [16]byte{}, [16]byte{}, genomesMap, nil } onlyExcludeConflictsGenomeIdentifier := inputGeneticAnalysis.OnlyExcludeConflictsGenomeIdentifier onlyIncludeSharedGenomeIdentifier := inputGeneticAnalysis.OnlyIncludeSharedGenomeIdentifier - return allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, nil + return allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, genomesMap, nil } //Outputs: @@ -178,7 +181,7 @@ func GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPerson1 bool, return inputGenomeIdentifier, true, false, "", nil } - _, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := GetMetadataFromPersonGeneticAnalysis(person1AnalysisObject) + _, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := GetMetadataFromPersonGeneticAnalysis(person1AnalysisObject) if (err != nil) { return [16]byte{}, false, false, "", err } if (multipleGenomesExist == false){ return [16]byte{}, false, false, "", errors.New("Couple analysis says person has multiple genomes, person analysis does not.") @@ -204,7 +207,7 @@ func GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPerson1 bool, return inputGenomeIdentifier, true, false, "", nil } - _, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := GetMetadataFromPersonGeneticAnalysis(person2AnalysisObject) + _, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := GetMetadataFromPersonGeneticAnalysis(person2AnalysisObject) if (err != nil) { return [16]byte{}, false, false, "", err } if (multipleGenomesExist == false){ return [16]byte{}, false, false, "", errors.New("Couple analysis says person has multiple genomes, person analysis does not.") @@ -226,9 +229,9 @@ func GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPerson1 bool, // -bool: Person has disease // -int: Probability of passing a disease variant // -string: Probability of passing a disease variant formatted (with % suffix) -// -int: Number of variants tested -// -int: Number of loci tested -// -int: Number of phased loci +// -int: Quantity of variants tested +// -int: Quantity of loci tested +// -int: Quantity of phased loci // -bool: Conflict exists // -error func GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, diseaseName string, genomeIdentifier [16]byte)(bool, bool, int, string, int, int, int, bool, error){ @@ -250,16 +253,16 @@ func GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject genet conflictExists := personMonogenicDiseaseInfo.ConflictExists personHasDisease := genomeMonogenicDiseaseInfo.PersonHasDisease - numberOfVariantsTested := genomeMonogenicDiseaseInfo.NumberOfVariantsTested - numberOfLociTested := genomeMonogenicDiseaseInfo.NumberOfLociTested - numberOfPhasedLoci := genomeMonogenicDiseaseInfo.NumberOfPhasedLoci + quantityOfVariantsTested := genomeMonogenicDiseaseInfo.QuantityOfVariantsTested + quantityOfLociTested := genomeMonogenicDiseaseInfo.QuantityOfLociTested + quantityOfPhasedLoci := genomeMonogenicDiseaseInfo.QuantityOfPhasedLoci probabilityOfPassingAVariant := genomeMonogenicDiseaseInfo.ProbabilityOfPassingADiseaseVariant probabilityOfPassingAVariantString := helpers.ConvertIntToString(probabilityOfPassingAVariant) probabilityOfPassingAVariantFormatted := probabilityOfPassingAVariantString + "%" - return true, personHasDisease, probabilityOfPassingAVariant, probabilityOfPassingAVariantFormatted, numberOfVariantsTested, numberOfLociTested, numberOfPhasedLoci, conflictExists, nil + return true, personHasDisease, probabilityOfPassingAVariant, probabilityOfPassingAVariantFormatted, quantityOfVariantsTested, quantityOfLociTested, quantityOfPhasedLoci, conflictExists, nil } @@ -485,24 +488,23 @@ func GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisOb // -bool: Polygenic Disease Risk Score known (any loci values exist) // -int: Person Disease risk score // -string: Person Disease risk score formatted (has "/10" suffix) -// -map[int]locusValue.LocusValue: Person locus values map -// -int: Number of loci tested +// -int: Quantity of loci tested // -bool: Conflict exists // -error -func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, diseaseName string, genomeIdentifier [16]byte)(bool, int, string, map[int64]locusValue.LocusValue, int, bool, error){ +func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, diseaseName string, genomeIdentifier [16]byte)(bool, int, string, int, bool, error){ personPolygenicDiseasesMap := personAnalysisObject.PolygenicDiseasesMap personPolygenicDiseaseInfo, exists := personPolygenicDiseasesMap[diseaseName] if (exists == false){ - return false, 0, "", nil, 0, false, nil + return false, 0, "", 0, false, nil } personPolygenicDiseaseInfoMap := personPolygenicDiseaseInfo.PolygenicDiseaseInfoMap genomePolygenicDiseaseInfo, exists := personPolygenicDiseaseInfoMap[genomeIdentifier] if (exists == false){ - return false, 0, "", nil, 0, false, nil + return false, 0, "", 0, false, nil } conflictExists := personPolygenicDiseaseInfo.ConflictExists @@ -513,11 +515,9 @@ func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject genet personDiseaseRiskScoreFormatted := personDiseaseRiskScoreString + "/10" - personLocusValuesMap := genomePolygenicDiseaseInfo.LocusValuesMap + quantityOfLociTested := genomePolygenicDiseaseInfo.QuantityOfLociTested - numberOfLociTested := genomePolygenicDiseaseInfo.NumberOfLociTested - - return true, personDiseaseRiskScore, personDiseaseRiskScoreFormatted, personLocusValuesMap, numberOfLociTested, conflictExists, nil + return true, personDiseaseRiskScore, personDiseaseRiskScoreFormatted, quantityOfLociTested, conflictExists, nil } @@ -526,7 +526,7 @@ func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject genet // -int: Offspring average disease risk score // -string: Offspring Disease average risk score formatted (has "/10" suffix) // -[]int: Sample Offspring Risk Scores List -// -int: Number of loci tested +// -int: Quantity of loci tested // -bool: Conflict exists // -error func GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, diseaseName string, genomePairIdentifier [32]byte)(bool, int, string, []int, int, bool, error){ @@ -547,7 +547,7 @@ func GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject ge conflictExists := couplePolygenicDiseaseInfo.ConflictExists - numberOfLociTested := genomePairPolygenicDiseaseInfo.NumberOfLociTested + quantityOfLociTested := genomePairPolygenicDiseaseInfo.QuantityOfLociTested offspringAverageRiskScore := genomePairPolygenicDiseaseInfo.OffspringAverageRiskScore @@ -557,7 +557,7 @@ func GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject ge sampleOffspringRiskScoresList := genomePairPolygenicDiseaseInfo.SampleOffspringRiskScoresList - return true, offspringAverageRiskScore, offspringAverageRiskScoreFormatted, sampleOffspringRiskScoresList, numberOfLociTested, conflictExists, nil + return true, offspringAverageRiskScore, offspringAverageRiskScoreFormatted, sampleOffspringRiskScoresList, quantityOfLociTested, conflictExists, nil } //Outputs: @@ -673,71 +673,169 @@ func GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisObje } //Outputs: -// -map[int64]locusValue.LocusValue (rsID -> Base pair) (missing rsIDs represent unknown values) +// -bool: Neural network exists +// -bool: Any neural network analysis exists +// -string: Neural network predicted outcome +// -int: Prediction confidence +// -int: Quantity of loci known (Neural network) +// -int: Quantity of phased loci (Neural network) +// -bool: Any trait rules exist (Rule-based analysis is possible) // -bool: Any Trait Rule known/tested -// -map[string]int: Trait outcomes scores map (Outcome -> Number of points) -// -int: Number of rules tested -// -bool: Conflict exists +// -map[[3]byte]bool: Rule Identifier -> Person passes rule +// -bool: Predicted outcome exists +// -string: Predicted outcome +// -int: Quantity of rules tested +// -int: Quantity Of Loci Known (Rules) +// -bool: Conflict exists (between any of these results for each genome) // -error -func GetPersonTraitInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, genomeIdentifier [16]byte)(map[int64]locusValue.LocusValue, bool, map[string]int, int, bool, error){ +func GetPersonDiscreteTraitInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, genomeIdentifier [16]byte)(bool, bool, string, int, int, int, bool, bool, map[[3]byte]bool, bool, string, int, int, bool, error){ - personTraitsMap := personAnalysisObject.TraitsMap + personTraitsMap := personAnalysisObject.DiscreteTraitsMap personTraitInfoObject, exists := personTraitsMap[traitName] if (exists == false){ - emptyMap := make(map[int64]locusValue.LocusValue) - return emptyMap, false, nil, 0, false, nil + return false, false, "", 0, 0, 0, false, false, nil, false, "", 0, 0, false, errors.New("Person trait analysis is missing trait: " + traitName) } personTraitInfoMap := personTraitInfoObject.TraitInfoMap + conflictExists := personTraitInfoObject.ConflictExists personGenomeTraitInfoObject, exists := personTraitInfoMap[genomeIdentifier] if (exists == false){ - emptyMap := make(map[int64]locusValue.LocusValue) - return emptyMap, false, nil, 0, false, nil + return false, false, "", 0, 0, 0, false, false, nil, false, "", 0, 0, false, errors.New("personTraitInfoMap in Person analysis is missing map for genome identifier.") } - conflictExists := personTraitInfoObject.ConflictExists + neuralNetworkExists := personGenomeTraitInfoObject.NeuralNetworkExists + neuralNetworkAnalysisExists := personGenomeTraitInfoObject.NeuralNetworkAnalysisExists - genomeNumberOfRulesTested := personGenomeTraitInfoObject.NumberOfRulesTested + getNeuralNetworkAnalysisInfo := func()(string, int, int, int){ - genomeLocusValuesMap := personGenomeTraitInfoObject.LocusValuesMap + if (neuralNetworkExists == false || neuralNetworkAnalysisExists == false){ + return "", 0, 0, 0 + } - genomeOutcomeScoresMap := personGenomeTraitInfoObject.OutcomeScoresMap + neuralNetworkAnalysis := personGenomeTraitInfoObject.NeuralNetworkAnalysis - return genomeLocusValuesMap, true, genomeOutcomeScoresMap, genomeNumberOfRulesTested, conflictExists, nil + predictedOutcome := neuralNetworkAnalysis.PredictedOutcome + predictionConfidence := neuralNetworkAnalysis.PredictionConfidence + quantityOfLociKnown := neuralNetworkAnalysis.QuantityOfLociKnown + quantityOfPhasedLoci := neuralNetworkAnalysis.QuantityOfPhasedLoci + + return predictedOutcome, predictionConfidence, quantityOfLociKnown, quantityOfPhasedLoci + } + + neuralNetworkPredictedOutcome, neuralNetworkPredictionConfidence, quantityOfLociKnown_NeuralNetwork, quantityOfPhasedLoci_NeuralNetwork := +getNeuralNetworkAnalysisInfo() + + anyRulesExist := personGenomeTraitInfoObject.AnyRulesExist + rulesAnalysisExists := personGenomeTraitInfoObject.RulesAnalysisExists + + getTraitAnalysisInfo := func()(map[[3]byte]bool, bool, string, int, int){ + + if (anyRulesExist == false || rulesAnalysisExists == false){ + return nil, false, "", 0, 0 + } + + rulesAnalysisObject := personGenomeTraitInfoObject.RulesAnalysis + + genomePassesRulesMap := rulesAnalysisObject.GenomePassesRulesMap + + predictedOutcomeExists := rulesAnalysisObject.PredictedOutcomeExists + + predictedOutcome := rulesAnalysisObject.PredictedOutcome + + quantityOfRulesTested := rulesAnalysisObject.QuantityOfRulesTested + + quantityOfLociKnown := rulesAnalysisObject.QuantityOfLociKnown + + return genomePassesRulesMap, predictedOutcomeExists, predictedOutcome, quantityOfRulesTested, quantityOfLociKnown + } + + genomePassesRulesMap, rulesPredictedOutcomeExists, rulesPredictedOutcome, quantityOfRulesTested, quantityOfLociKnown_Rules := getTraitAnalysisInfo() + + return neuralNetworkExists, neuralNetworkAnalysisExists, neuralNetworkPredictedOutcome, neuralNetworkPredictionConfidence, quantityOfLociKnown_NeuralNetwork, quantityOfPhasedLoci_NeuralNetwork, anyRulesExist, rulesAnalysisExists, genomePassesRulesMap, rulesPredictedOutcomeExists, rulesPredictedOutcome, quantityOfRulesTested, quantityOfLociKnown_Rules, conflictExists, nil } - - //Outputs: -// -bool: Trait Outcome Scores known -// -map[string]float64: Trait average outcome scores map (OutcomeName -> AverageScore) -// -int: Number of rules tested -// -bool: Conflict exists +// -bool: Neural network exists +// -bool: Neural network analysis exists +// -map[string]int: Offspring outcome probabilities map for neural network prediction +// -Map Structure: Outcome name -> Probability of outcome (0-100) +// -int: Average confidence (for neural network prediction) +// -int: Quantity of loci known (for neural network) +// -int: Quantity of Parental phased loci +// -bool: Any Rules exist +// -bool: Rules analysis exists +// -map[string]int: Offspring outcome probabilities map for rules-based prediction +// -Map Structure: Outcome name -> Probability of outcome (0-100) +// -map[[3]byte]int: Offspring probability of passing rules map +// -Map Structure: Rule Identifier -> Probability of passing rule (0-100) +// -int: Quantity of rules tested +// -int: Quantity of loci known (For Rules) +// -bool: Conflict exists (Between this genome pair and other genome pairs) // -error -func GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte)(bool, map[string]float64, int, bool, error){ +func GetOffspringDiscreteTraitInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, genomePairIdentifier [32]byte)(bool, bool, map[string]int, int, int, int, bool, bool, map[string]int, map[[3]byte]int, int, int, bool, error){ - offspringTraitsMap := coupleAnalysisObject.TraitsMap + offspringTraitsMap := coupleAnalysisObject.DiscreteTraitsMap traitInfoObject, exists := offspringTraitsMap[traitName] if (exists == false){ - return false, nil, 0, false, nil + return false, false, nil, 0, 0, 0, false, false, nil, nil, 0, 0, false, errors.New("offspringTraitsMap missing trait when reading couple genetic analysis: " + traitName) } traitInfoMap := traitInfoObject.TraitInfoMap + conflictExists := traitInfoObject.ConflictExists genomePairTraitInfoObject, exists := traitInfoMap[genomePairIdentifier] if (exists == false){ - return false, nil, 0, false, nil + return false, false, nil, 0, 0, 0, false, false, nil, nil, 0, 0, false, errors.New("traitInfoMap missing trait info for genome pair when reading from genetic analysis.") } - conflictExists := traitInfoObject.ConflictExists + neuralNetworkExists := genomePairTraitInfoObject.NeuralNetworkExists - numberOfRulesTested := genomePairTraitInfoObject.NumberOfRulesTested - offspringAverageOutcomeScoresMap := genomePairTraitInfoObject.OffspringAverageOutcomeScoresMap + neuralNetworkAnalysisExists := genomePairTraitInfoObject.NeuralNetworkAnalysisExists - return true, offspringAverageOutcomeScoresMap, numberOfRulesTested, conflictExists, nil + getGenomePairTraitNeuralNetworkAnalysisInfo := func()(map[string]int, int, int, int){ + + if (neuralNetworkExists == false || neuralNetworkAnalysisExists == false){ + return nil, 0, 0, 0 + } + + genomePairTraitNeuralNetworkInfo := genomePairTraitInfoObject.NeuralNetworkAnalysis + + offspringOutcomeProbabilitiesMap := genomePairTraitNeuralNetworkInfo.OffspringOutcomeProbabilitiesMap + averageConfidence := genomePairTraitNeuralNetworkInfo.AverageConfidence + quantityOfLociKnown := genomePairTraitNeuralNetworkInfo.QuantityOfLociKnown + quantityOfParentalPhasedLoci := genomePairTraitNeuralNetworkInfo.QuantityOfParentalPhasedLoci + + return offspringOutcomeProbabilitiesMap, averageConfidence, quantityOfLociKnown, quantityOfParentalPhasedLoci + } + + offspringOutcomeProbabilitiesMap_NeuralNetwork, averageConfidence_NeuralNetwork, quantityOfLociKnown_NeuralNetwork, quantityOfParentalPhasedLoci_NeuralNetwork := getGenomePairTraitNeuralNetworkAnalysisInfo() + + anyRulesExist := genomePairTraitInfoObject.RulesExist + + rulesAnalysisExists := genomePairTraitInfoObject.RulesAnalysisExists + + getGenomePairTraitRulesAnalysisInfo := func()(map[string]int, map[[3]byte]int, int, int){ + + if (anyRulesExist == false || rulesAnalysisExists == false){ + return nil, nil, 0, 0 + } + + genomePairTraitInfo_Rules := genomePairTraitInfoObject.RulesAnalysis + + offspringOutcomeProbabilitiesMap := genomePairTraitInfo_Rules.OffspringOutcomeProbabilitiesMap + probabilityOfPassingRulesMap := genomePairTraitInfo_Rules.ProbabilityOfPassingRulesMap + quantityOfRulesTested := genomePairTraitInfo_Rules.QuantityOfRulesTested + quantityOfLociKnown := genomePairTraitInfo_Rules.QuantityOfLociKnown + + return offspringOutcomeProbabilitiesMap, probabilityOfPassingRulesMap, quantityOfRulesTested, quantityOfLociKnown + } + + offspringOutcomeProbabilitiesMap_Rules, probabilityOfPassingRulesMap, quantityOfRulesTested, quantityOfLociKnown_Rules := getGenomePairTraitRulesAnalysisInfo() + + return neuralNetworkExists, neuralNetworkAnalysisExists, offspringOutcomeProbabilitiesMap_NeuralNetwork, averageConfidence_NeuralNetwork, quantityOfLociKnown_NeuralNetwork, quantityOfParentalPhasedLoci_NeuralNetwork, anyRulesExist, rulesAnalysisExists, offspringOutcomeProbabilitiesMap_Rules, probabilityOfPassingRulesMap, quantityOfRulesTested, quantityOfLociKnown_Rules, conflictExists, nil } @@ -745,24 +843,17 @@ func GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalys // -bool: Rule status is known (we know if the rule is passed or not) // -bool: Genome passes rule // -error -func GetPersonTraitRuleInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, ruleIdentifier [3]byte, genomeIdentifier [16]byte)(bool, bool, error){ +func GetPersonDiscreteTraitRuleInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, traitName string, ruleIdentifier [3]byte, genomeIdentifier [16]byte)(bool, bool, error){ - personTraitsMap := personAnalysisObject.TraitsMap - - traitInfoObject, exists := personTraitsMap[traitName] - if (exists == false){ + _, _, _, _, _, _, anyRulesExist, rulesAnalysisExists, genomePassesRulesMap, _, _, _, _, _, err := GetPersonDiscreteTraitInfoFromGeneticAnalysis(personAnalysisObject, traitName, genomeIdentifier) + if (err != nil) { return false, false, err } + if (anyRulesExist == false){ + return false, false, errors.New("GetPersonTraitRuleInfoFromGeneticAnalysis called when no trait rules exist.") + } + if (rulesAnalysisExists == false){ return false, false, nil } - personTraitInfoMap := traitInfoObject.TraitInfoMap - - genomeTraitInfoObject, exists := personTraitInfoMap[genomeIdentifier] - if (exists == false){ - return false, false, nil - } - - genomePassesRulesMap := genomeTraitInfoObject.GenomePassesRulesMap - genomePassesRule, statusIsKnown := genomePassesRulesMap[ruleIdentifier] if (statusIsKnown == false){ return false, false, nil @@ -777,24 +868,17 @@ func GetPersonTraitRuleInfoFromGeneticAnalysis(personAnalysisObject geneticAnaly // -int: Offspring probability of passing rule (0 - 100) // -string: Offspring probability of passing rule formatted (with % suffix) // -error -func GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, ruleIdentifier [3]byte, genomePairIdentifier [32]byte)(bool, int, string, error){ +func GetOffspringDiscreteTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, traitName string, ruleIdentifier [3]byte, genomePairIdentifier [32]byte)(bool, int, string, error){ - offspringTraitsMap := coupleAnalysisObject.TraitsMap - - offspringTraitInfo, exists := offspringTraitsMap[traitName] - if (exists == false){ + _, _, _, _, _, _, anyRulesExist, rulesAnalysisExists, _, offspringProbabilityOfPassingRulesMap, _, _, _, err := GetOffspringDiscreteTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) + if (err != nil) { return false, 0, "", err } + if (anyRulesExist == false){ + return false, 0, "", errors.New("GetOffspringTraitRuleInfoFromGeneticAnalysis called for trait which has no rules: " + traitName) + } + if (rulesAnalysisExists == false){ return false, 0, "", nil } - offspringTraitInfoMap := offspringTraitInfo.TraitInfoMap - - offspringTraitInfoObject, exists := offspringTraitInfoMap[genomePairIdentifier] - if (exists == false){ - return false, 0, "", nil - } - - offspringProbabilityOfPassingRulesMap := offspringTraitInfoObject.ProbabilityOfPassingRulesMap - offspringProbabilityOfPassingRule, exists := offspringProbabilityOfPassingRulesMap[ruleIdentifier] if (exists == false){ return false, 0, "", nil @@ -812,7 +896,7 @@ func GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject geneticAn //TODO: Perform sanity checks on data func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis)error{ - allRawGenomeIdentifiersList, personHasMultipleGenomes, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := GetMetadataFromPersonGeneticAnalysis(personAnalysisObject) + allRawGenomeIdentifiersList, personHasMultipleGenomes, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, _, err := GetMetadataFromPersonGeneticAnalysis(personAnalysisObject) if (err != nil) { return err } allGenomeIdentifiersList := allRawGenomeIdentifiersList @@ -859,7 +943,7 @@ func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnal for _, genomeIdentifier := range allGenomeIdentifiersList{ - _, _, _, _, _, _, err := GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, genomeIdentifier) + _, _, _, _, _, err := GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, genomeIdentifier) if (err != nil) { return err } } @@ -886,27 +970,31 @@ func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnal for _, traitObject := range traitObjectsList{ traitName := traitObject.TraitName + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric - for _, genomeIdentifier := range allGenomeIdentifiersList{ - - _, _, _, _, _, err := GetPersonTraitInfoFromGeneticAnalysis(personAnalysisObject, traitName, genomeIdentifier) - if (err != nil) { return err } - } - - traitRulesList := traitObject.RulesList - - for _, traitRuleObject := range traitRulesList{ - - ruleIdentifierHex := traitRuleObject.RuleIdentifier - - ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) - if (err != nil) { return err } + if (traitIsDiscreteOrNumeric == "Discrete"){ for _, genomeIdentifier := range allGenomeIdentifiersList{ - _, _, err := GetPersonTraitRuleInfoFromGeneticAnalysis(personAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) + _, _, _, _, _, _, _, _, _, _, _, _, _, _, err := GetPersonDiscreteTraitInfoFromGeneticAnalysis(personAnalysisObject, traitName, genomeIdentifier) if (err != nil) { return err } } + + traitRulesList := traitObject.RulesList + + for _, traitRuleObject := range traitRulesList{ + + ruleIdentifierHex := traitRuleObject.RuleIdentifier + + ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) + if (err != nil) { return err } + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, err := GetPersonDiscreteTraitRuleInfoFromGeneticAnalysis(personAnalysisObject, traitName, ruleIdentifier, genomeIdentifier) + if (err != nil) { return err } + } + } } } @@ -998,27 +1086,31 @@ func VerifyCoupleGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnal for _, traitObject := range traitObjectsList{ traitName := traitObject.TraitName + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric - for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + if (traitIsDiscreteOrNumeric == "Discrete"){ - _, _, _, _, err := GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) - if (err != nil) { return err } - } + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ - traitRulesList := traitObject.RulesList - - for _, traitRuleObject := range traitRulesList{ - - ruleIdentifierHex := traitRuleObject.RuleIdentifier - - ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) - if (err != nil) { return err } - - for _, genomePairIdentifier := range allGenomePairIdentifiersList{ - - _, _, _, err := GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) + _, _, _, _, _, _, _, _, _, _, _, _, _, err := GetOffspringDiscreteTraitInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, genomePairIdentifier) if (err != nil) { return err } } + + traitRulesList := traitObject.RulesList + + for _, traitRuleObject := range traitRulesList{ + + ruleIdentifierHex := traitRuleObject.RuleIdentifier + + ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) + if (err != nil) { return err } + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, err := GetOffspringDiscreteTraitRuleInfoFromGeneticAnalysis(coupleAnalysisObject, traitName, ruleIdentifier, genomePairIdentifier) + if (err != nil) { return err } + } + } } } diff --git a/internal/genetics/readRawGenomes/readRawGenomes.go b/internal/genetics/readRawGenomes/readRawGenomes.go index 056879f..5d4c54b 100644 --- a/internal/genetics/readRawGenomes/readRawGenomes.go +++ b/internal/genetics/readRawGenomes/readRawGenomes.go @@ -128,41 +128,44 @@ func ReadRawGenomeFile(fileReader io.Reader) (string, int, int64, int64, bool, m } getMonthObject := func()(time.Month, error){ - if (monthString == "01"){ - return time.January, nil - } - if (monthString == "02"){ - return time.February, nil - } - if (monthString == "03"){ - return time.March, nil - } - if (monthString == "04"){ - return time.April, nil - } - if (monthString == "05"){ - return time.May, nil - } - if (monthString == "06"){ - return time.June, nil - } - if (monthString == "07"){ - return time.July, nil - } - if (monthString == "08"){ - return time.August, nil - } - if (monthString == "09"){ - return time.September, nil - } - if (monthString == "10"){ - return time.October, nil - } - if (monthString == "11"){ - return time.November, nil - } - if (monthString == "12"){ - return time.December, nil + + switch monthString{ + case "01":{ + return time.January, nil + } + case "02":{ + return time.February, nil + } + case "03":{ + return time.March, nil + } + case "04":{ + return time.April, nil + } + case "05":{ + return time.May, nil + } + case "06":{ + return time.June, nil + } + case "07":{ + return time.July, nil + } + case "08":{ + return time.August, nil + } + case "09":{ + return time.September, nil + } + case "10":{ + return time.October, nil + } + case "11":{ + return time.November, nil + } + case "12":{ + return time.December, nil + } } return time.January, errors.New("Malformed AncestryDNA genome file: Invalid month: " + monthString) } @@ -438,15 +441,22 @@ func ReadRawGenomeFile(fileReader io.Reader) (string, int, int64, int64, bool, m snpIdentifier := rowSlice[0] snpValueRaw := rowSlice[3] - if (snpValueRaw[0] != byte('-')){ - // Locus value is not "--" - // Locus value exists - numberOfLoci += 1 + if (len(snpValueRaw) < 2){ + return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome data: Invalid SNP row snp value: " + fileLineString) } + if (snpValueRaw[0] == '-'){ + // Locus value is "--" + // Locus value does not exist + continue + } + + numberOfLoci += 1 + //Outputs: // -bool: rsID found // -int64: rsID value + // -error getRSIDIdentifier := func()(bool, int64, error){ isRSID, rsidInt64 := readRSIDString(snpIdentifier) @@ -481,75 +491,50 @@ func ReadRawGenomeFile(fileReader io.Reader) (string, int, int64, int64, bool, m continue } - // This will return either a base pair or a single base - // Base pair can be "--" - getLocusValueString := func()(string, error){ + getLocusBasesList := func()([]rune, error){ - // This value has a control character suffix - // Final index is always a control character - // We remove the control character suffix + locusBasesList := make([]rune, 0) - if (len(snpValueRaw) == 2){ + finalIndex := len(snpValueRaw) - 1 - singleBase := string(snpValueRaw[0]) - return singleBase, nil - } + for index, character := range snpValueRaw{ - if (len(snpValueRaw) == 3){ + baseIsValid := verifyBase(string(character)) + if (baseIsValid == false){ - basePair := snpValueRaw[:2] - return basePair, nil - } + if (index == finalIndex){ + // The final index of snpValueRaw is sometimes a control character - return "", errors.New("Malformed 23andMe genome file: Invalid SNP value: " + snpValueRaw) - } + return locusBasesList, nil + } - basesString, err := getLocusValueString() - if (err != nil) { return "", 0, 0, 0, false, nil, err } - - if (basesString == "--"){ - // No data exists, skip. - continue - } - - for _, baseRune := range basesString{ - - baseIsValid := verifyBase(string(baseRune)) - if (baseIsValid == false){ - return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome file: Invalid SNP base: " + string(baseRune)) - } - } - - getMapEntryValue := func()RawGenomeLocusValue{ - - if (len(basesString) == 1){ - - locusValueObject := RawGenomeLocusValue{ - - Allele1: basesString, - Allele2Exists: false, - Allele2: "", + return nil, errors.New("Malformed 23andMe genome file: Invalid SNP base: " + string(character)) } - return locusValueObject + locusBasesList = append(locusBasesList, character) } - baseAString := string(basesString[0]) - baseBString := string(basesString[1]) - - locusValueObject := RawGenomeLocusValue{ - - Allele1: baseAString, - Allele2Exists: true, - Allele2: baseBString, - } - - return locusValueObject + return locusBasesList, nil } - mapEntryValue := getMapEntryValue() + locusBasesList, err := getLocusBasesList() + if (err != nil){ return "", 0, 0, 0, false, nil, err } - genomeMap[locusRSID] = mapEntryValue + allele1 := string(locusBasesList[0]) + + locusValueObject := RawGenomeLocusValue{ + Allele1: allele1, + } + + if (len(locusBasesList) > 1){ + + allele2 := string(locusBasesList[1]) + + locusValueObject.Allele2Exists = true + locusValueObject.Allele2 = allele2 + } + + genomeMap[locusRSID] = locusValueObject } return "23andMe", 1, fileTimeUnix, numberOfLoci, false, genomeMap, nil diff --git a/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack index 2de4ccb..558c216 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 3748396..dfe7e28 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 bf99e65..dc32f26 100644 Binary files a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack and b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack differ diff --git a/internal/profiles/calculatedAttributes/calculatedAttributes.go b/internal/profiles/calculatedAttributes/calculatedAttributes.go index 10ceb30..b2f8279 100644 --- a/internal/profiles/calculatedAttributes/calculatedAttributes.go +++ b/internal/profiles/calculatedAttributes/calculatedAttributes.go @@ -769,6 +769,14 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA return false, profileVersion, "", nil } + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject) + if (err != nil) { return false, 0, "", err } + + myGenomeLocusValuesMap, exists := myGenomesMap[myGenomeIdentifier] + if (exists == false){ + return false, 0, "", errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis which has GenomesMap which is missing my genome identifier.") + } + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() if (err != nil) { return false, 0, "", err } @@ -780,12 +788,8 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA for _, diseaseObject := range polygenicDiseaseObjectsList{ - diseaseName := diseaseObject.DiseaseName diseaseLociList := diseaseObject.LociList - _, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, myGenomeIdentifier) - if (err != nil) { return false, 0, "", err } - // Map Structure: rsID -> Locus Value userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) @@ -818,7 +822,7 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA userDiseaseLocusValuesMap[locusRSID] = newLocusValue } - anyLocusValuesTested, offspringAverageRiskScore, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo_Fast(diseaseLociList, myDiseaseLocusValuesMap, userDiseaseLocusValuesMap) + anyLocusValuesTested, offspringAverageRiskScore, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo_Fast(diseaseLociList, myGenomeLocusValuesMap, userDiseaseLocusValuesMap) if (err != nil) { return false, 0, "", err } if (anyLocusValuesTested == false){ continue @@ -1399,9 +1403,14 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA traitName := getTraitName() - myTraitLociMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myGeneticAnalysisObject, traitName, myGenomeIdentifier) + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject) if (err != nil) { return false, 0, "", err } + myGenomeLocusValuesMap, exists := myGenomesMap[myGenomeIdentifier] + if (exists == false){ + return false, 0, "", errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis with a GenomesMap which is missing my genome identifier.") + } + traitObject, err := traits.GetTraitObject(traitName) if (err != nil) { return false, 0, "", err } @@ -1415,7 +1424,7 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA for _, rsID := range traitLociList{ - myLocusValue, myLocusValueExists := myTraitLociMap[rsID] + myLocusValue, myLocusValueExists := myGenomeLocusValuesMap[rsID] if (myLocusValueExists == false){ continue } diff --git a/internal/profiles/myProfileExports/myProfileExports.go b/internal/profiles/myProfileExports/myProfileExports.go index 970a6ce..c6c6c28 100644 --- a/internal/profiles/myProfileExports/myProfileExports.go +++ b/internal/profiles/myProfileExports/myProfileExports.go @@ -150,6 +150,14 @@ func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ return errors.New("UpdateMyExportedProfile called when profile genetic analysis is not ready.") } + _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject) + if (err != nil) { return err } + + myGenomeLocusValuesMap, exists := myGenomesMap[genomeIdentifierToShare] + if (exists == false){ + return errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis which has GenomesMap which is missing my genome identifier.") + } + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() if (err != nil) { return err } @@ -205,19 +213,24 @@ func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ continue } - _, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, genomeIdentifierToShare) - if (err != nil) { return err } + lociList := diseaseObject.LociList - for rsID, locusValueObject := range myDiseaseLocusValuesMap{ + for _, locusObject := range lociList{ - rsIDString := helpers.ConvertInt64ToString(rsID) + locusRSID := locusObject.LocusRSID - locusBase1 := locusValueObject.Base1Value - locusBase2 := locusValueObject.Base2Value + locusValueObject, exists := myGenomeLocusValuesMap[locusRSID] + if (exists == true){ - basePairValue := locusBase1 + ";" + locusBase2 + rsIDString := helpers.ConvertInt64ToString(locusRSID) - profileMap["LocusValue_rs" + rsIDString] = basePairValue + locusBase1 := locusValueObject.Base1Value + locusBase2 := locusValueObject.Base2Value + + basePairValue := locusBase1 + ";" + locusBase2 + + profileMap["LocusValue_rs" + rsIDString] = basePairValue + } } } @@ -239,19 +252,22 @@ func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ continue } - myTraitLocusValuesMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myGeneticAnalysisObject, traitName, genomeIdentifierToShare) - if (err != nil) { return err } + lociList := traitObject.LociList - for rsID, locusValueObject := range myTraitLocusValuesMap{ + for _, rsID := range lociList{ - rsIDString := helpers.ConvertInt64ToString(rsID) + locusValueObject, exists := myGenomeLocusValuesMap[rsID] + if (exists == true){ - locusBase1 := locusValueObject.Base1Value - locusBase2 := locusValueObject.Base2Value + rsIDString := helpers.ConvertInt64ToString(rsID) - basePairValue := locusBase1 + ";" + locusBase2 + locusBase1 := locusValueObject.Base1Value + locusBase2 := locusValueObject.Base2Value - profileMap["LocusValue_rs" + rsIDString] = basePairValue + basePairValue := locusBase1 + ";" + locusBase2 + + profileMap["LocusValue_rs" + rsIDString] = basePairValue + } } } diff --git a/resources/geneticPredictionModels/geneticPredictionModels.go b/resources/geneticPredictionModels/geneticPredictionModels.go new file mode 100644 index 0000000..6401a41 --- /dev/null +++ b/resources/geneticPredictionModels/geneticPredictionModels.go @@ -0,0 +1,58 @@ +// geneticPredictionModels contains genetic prediction neural network models for predicting genetic traits +// These are .gob encoded files of []float32 weights +// This package also contains prediction accuracy information for each model +// Prediction accuracy models describe information about how accurate the predictions made by the models are +// All of the files in this package are created by the Create Genetic Models utility. +// This utility is located in /utilities/createGeneticModels/createGeneticModels.go + +package geneticPredictionModels + +import _ "embed" + +import "errors" + +//Outputs: +// -bool: Model exists +// -[]byte +func GetGeneticPredictionModelBytes(traitName string)(bool, []byte){ + + switch traitName{ + + case "Eye Color":{ + return true, predictionModel_EyeColor + } + case "Lactose Tolerance":{ + return true, predictionModel_LactoseTolerance + } + } + + return false, nil +} + +//go:embed predictionModels/EyeColorModel.gob +var predictionModel_EyeColor []byte + +//go:embed predictionModels/LactoseToleranceModel.gob +var predictionModel_LactoseTolerance []byte + +// The files returned by this function are .gob encoded geneticPrediction.TraitPredictionAccuracyInfoMap objects +func GetPredictionModelTraitAccuracyInfoBytes(traitName string)([]byte, error){ + + switch traitName{ + case "Eye Color":{ + return predictionAccuracy_EyeColor, nil + } + case "Lactose Tolerance":{ + return predictionAccuracy_LactoseTolerance, nil + } + } + + return nil, errors.New("GetPredictionModelTraitAccuracyInfoFile called with unknown traitName: " + traitName) +} + +//go:embed predictionModelAccuracies/EyeColorModelAccuracy.gob +var predictionAccuracy_EyeColor []byte + +//go:embed predictionModelAccuracies/LactoseToleranceModelAccuracy.gob +var predictionAccuracy_LactoseTolerance []byte + diff --git a/resources/geneticPredictionModels/geneticPredictionModels_test.go b/resources/geneticPredictionModels/geneticPredictionModels_test.go new file mode 100644 index 0000000..92ffc69 --- /dev/null +++ b/resources/geneticPredictionModels/geneticPredictionModels_test.go @@ -0,0 +1,48 @@ +package geneticPredictionModels_test + +import "seekia/resources/geneticPredictionModels" + +import "testing" + +import "seekia/internal/genetics/geneticPrediction" + + +func TestGeneticPredictionModels(t *testing.T){ + + traitNamesList := []string{"Eye Color", "Lactose Tolerance"} + + for _, traitName := range traitNamesList{ + + modelFound, modelBytes := geneticPredictionModels.GetGeneticPredictionModelBytes(traitName) + if (modelFound == false){ + t.Fatalf("GetGeneticPredictionModelBytes failed to find model for trait: " + traitName) + } + + _, err := geneticPrediction.DecodeBytesToNeuralNetworkObject(modelBytes) + if (err != nil){ + t.Fatalf("DecodeBytesToNeuralNetworkObject failed: " + err.Error()) + } + } +} + + +func TestGeneticPredictionModelAccuracies(t *testing.T){ + + traitNamesList := []string{"Eye Color", "Lactose Tolerance"} + + for _, traitName := range traitNamesList{ + + accuracyInfoBytes, err := geneticPredictionModels.GetPredictionModelTraitAccuracyInfoBytes(traitName) + if (err != nil){ + t.Fatalf("GetGeneticPredictionModelBytes failed: " + err.Error()) + } + + _, err = geneticPrediction.DecodeBytesToTraitPredictionAccuracyInfoMap(accuracyInfoBytes) + if (err != nil){ + t.Fatalf("DecodeBytesToTraitPredictionAccuracyInfoMap failed: " + err.Error()) + } + } +} + + + diff --git a/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob b/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob new file mode 100644 index 0000000..dd8cb7b Binary files /dev/null and b/resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob differ diff --git a/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob b/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob new file mode 100644 index 0000000..6672f75 Binary files /dev/null and b/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob differ diff --git a/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob b/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob new file mode 100644 index 0000000..414c2d1 Binary files /dev/null and b/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob differ diff --git a/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob b/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob new file mode 100644 index 0000000..a34a8a0 Binary files /dev/null and b/resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob differ diff --git a/resources/geneticReferences/geneticReferences_test.go b/resources/geneticReferences/geneticReferences_test.go index c76745c..2f5a7fe 100644 --- a/resources/geneticReferences/geneticReferences_test.go +++ b/resources/geneticReferences/geneticReferences_test.go @@ -309,10 +309,13 @@ func TestGeneticReferences(t *testing.T){ traitName := traitObject.TraitName traitDescription := traitObject.TraitDescription + traitDiscreteOrNumeric := traitObject.DiscreteOrNumeric + traitLocusReferencesMap := traitObject.LocusReferencesMap traitLociList := traitObject.LociList + traitLociList_Rules := traitObject.LociList_Rules traitRulesList := traitObject.RulesList traitOutcomesList := traitObject.OutcomesList - traitReferencesMap := traitObject.References + traitReferencesMap := traitObject.ReferencesMap if (traitName == ""){ t.Fatalf("Empty trait name exists.") @@ -326,6 +329,9 @@ func TestGeneticReferences(t *testing.T){ if (traitDescription == ""){ t.Fatalf("Empty trait description exists for trait: " + traitName) } + if (traitDiscreteOrNumeric != "Discrete" && traitDiscreteOrNumeric != "Numeric"){ + t.Fatalf("Invalid DiscreteOrNumeric for trait: " + traitDiscreteOrNumeric) + } if (len(traitOutcomesList) != 0){ if (len(traitOutcomesList) < 2){ @@ -349,18 +355,41 @@ func TestGeneticReferences(t *testing.T){ t.Fatalf("Invalid references exist for trait: " + traitName) } + if (len(traitLocusReferencesMap) == 0){ + t.Fatalf("No trait locus references exist for trait: " + traitName) + } + + for locusRSID, locusReferences := range traitLocusReferencesMap{ + + allRSIDsMap[locusRSID] = struct{}{} + + if (locusReferences == nil){ + t.Fatalf("A trait locus has no references map: " + traitName) + } + if (len(locusReferences) == 0){ + t.Fatalf("A trait locus has no references: " + traitName) + } + + locusExists := slices.Contains(traitLociList, locusRSID) + if (locusExists == false){ + t.Fatalf("traitLocusReferencesMap contains rsID which does not exist in traitLociList") + } + } + if (len(traitLociList) == 0){ t.Fatalf("No trait loci exist for trait: " + traitName) } - for _, locusRSID := range traitLociList{ - allRSIDsMap[locusRSID] = struct{}{} + for _, rsID := range traitLociList{ + allRSIDsMap[rsID] = struct{}{} } - containsDuplicates, duplicateLocus := helpers.CheckIfListContainsDuplicates(traitLociList) - if (containsDuplicates == true){ - duplicateLocusString := helpers.ConvertInt64ToString(duplicateLocus) - t.Fatalf("traitLociList contains duplicates for trait: " + traitName + ". RSID: " + duplicateLocusString) + for _, rsID := range traitLociList_Rules{ + + locusExists := slices.Contains(traitLociList, rsID) + if (locusExists == false){ + t.Fatalf("traitLociList_Rules contains locus not present in traitLociList") + } } if (len(traitRulesList) == 0){ @@ -373,7 +402,7 @@ func TestGeneticReferences(t *testing.T){ ruleIdentifier := ruleObject.RuleIdentifier ruleLociList := ruleObject.LociList ruleOutcomePointsMap := ruleObject.OutcomePointsMap - ruleReferences := ruleObject.References + ruleReferencesMap := ruleObject.ReferencesMap identifierIsValid := verifyIdentifier(ruleIdentifier) if (identifierIsValid == false){ @@ -413,11 +442,21 @@ func TestGeneticReferences(t *testing.T){ t.Fatalf("Trait rule Locus identifier is invalid: " + locusIdentifier) } - listContainsItem := slices.Contains(traitLociList, locusRSID) - if (listContainsItem == false){ + _, mapContainsItem := traitLocusReferencesMap[locusRSID] + if (mapContainsItem == false){ + t.Fatalf("Rule locus contains rsid which is not contained within LocusReferencesMap.") + } + + sliceContainsItem := slices.Contains(traitLociList, locusRSID) + if (sliceContainsItem == false){ t.Fatalf("Rule locus contains rsid which is not contained within traitLociList.") } + sliceContainsItem = slices.Contains(traitLociList_Rules, locusRSID) + if (sliceContainsItem == false){ + t.Fatalf("Rule locus contains rsid which is not contained within traitLociList_Rules.") + } + if (len(locusBasePairsList) == 0){ t.Fatalf("Trait rule locus base pairs list is empty: " + locusIdentifier) } @@ -430,7 +469,7 @@ func TestGeneticReferences(t *testing.T){ } } - referencesAreValid := verifyReferencesMap(ruleReferences) + referencesAreValid := verifyReferencesMap(ruleReferencesMap) if (referencesAreValid == false){ t.Fatalf("Invalid references map for trait rule locus: " + ruleIdentifier) } diff --git a/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go b/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go index f229384..24014b0 100644 --- a/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go +++ b/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go @@ -8,9 +8,8 @@ package polygenicDiseases // Polygenic disease probabilities are less accurate, because individual base pair changes only cause comparatively small changes in the disease risk. // Polygenic diseases are also more influenced by environmental factors, further decreasing risk accuracy. -//TODO: Eventually we want to use neural networks for both polygenic disease and trait prediction. -// This package is currently a temporary, less accurate solution until we get access to the necessary training data. -// It may still be worth keeping this method in place for polygenic diseases and using neural networks in tandem. +//TODO: Eventually we want to use neural networks for polygenic disease prediction. +// This package is currently a less accurate solution until we get access to the necessary training data. import "errors" diff --git a/resources/geneticReferences/traits/eyeColor.go b/resources/geneticReferences/traits/eyeColor.go index 402689c..e24439b 100644 --- a/resources/geneticReferences/traits/eyeColor.go +++ b/resources/geneticReferences/traits/eyeColor.go @@ -1,14 +1,20 @@ package traits +import "seekia/internal/helpers" + +import "maps" func getEyeColorTraitObject()Trait{ - eyeColorLociList := []int64{ + // Map Structure: rsID -> References Map + locusReferencesMap := make(map[int64]map[string]string) - //TODO: Add more SNPs. + referencesMap_List1 := make(map[string]string) + referencesMap_List1["SNPedia.com - Eye Color"] = "https://www.snpedia.com/index.php/Eye_color" - // These SNPs are taken from https://www.snpedia.com/index.php/Eye_color + // These SNPs are taken from https://www.snpedia.com/index.php/Eye_color + lociList_1 := []int64{ 2733832, 1800401, 1800407, @@ -51,16 +57,39 @@ func getEyeColorTraitObject()Trait{ 989869, 4778138, 12906280, + } - // These SNPs are taken from https://pubmed.ncbi.nlm.nih.gov/20546537/ + for _, rsID := range lociList_1{ + + locusReferencesMap[rsID] = maps.Clone(referencesMap_List1) + } + + referencesMap_List2 := make(map[string]string) + referencesMap_List2["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" + + // These SNPs are taken from https://pubmed.ncbi.nlm.nih.gov/20546537/ + + lociList_2 := []int64{ 12203592, 1408799, 1126809, 12896399, 7495174, 1667394, + } - // These SNPs are taken from https://pubmed.ncbi.nlm.nih.gov/33692100/ + for _, rsID := range lociList_2{ + + locusReferencesMap[rsID] = maps.Clone(referencesMap_List2) + } + + + referencesMap_List3 := make(map[string]string) + referencesMap_List3["Genome-wide association study in almost 195,000 individuals identifies 50 previously unidentified genetic loci for eye color."] = "https://pubmed.ncbi.nlm.nih.gov/33692100/" + + // These SNPs are taken from https://pubmed.ncbi.nlm.nih.gov/33692100/ + + lociList_3 := []int64{ 6693258, 351385, 2385028, @@ -115,6 +144,12 @@ func getEyeColorTraitObject()Trait{ // 5957354, } + for _, rsID := range lociList_3{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List3) + } + + eyeColorLociList := helpers.GetListOfMapKeys(locusReferencesMap) + referencesMap := make(map[string]string) referencesMap["SNPedia.com - Eye Color"] = "https://www.snpedia.com/index.php/Eye_color" referencesMap["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" @@ -123,10 +158,13 @@ func getEyeColorTraitObject()Trait{ eyeColorObject := Trait{ TraitName: "Eye Color", TraitDescription: "The color of a person's eyes.", + DiscreteOrNumeric: "Discrete", + LocusReferencesMap: locusReferencesMap, LociList: eyeColorLociList, + LociList_Rules: []int64{}, RulesList: []TraitRule{}, OutcomesList: []string{"Blue", "Green", "Hazel", "Brown"}, - References: referencesMap, + ReferencesMap: referencesMap, } return eyeColorObject diff --git a/resources/geneticReferences/traits/facialStructure.go b/resources/geneticReferences/traits/facialStructure.go index 51cb0fe..b5c2086 100644 --- a/resources/geneticReferences/traits/facialStructure.go +++ b/resources/geneticReferences/traits/facialStructure.go @@ -1,9 +1,19 @@ package traits +import "seekia/internal/helpers" + +import "maps" func getFacialStructureTraitObject()Trait{ - facialStructureLociList := []int64{ + // Map Structure: rsID -> References Map + locusReferencesMap := make(map[int64]map[string]string) + + referencesMap_List1 := make(map[string]string) + referencesMap_List1["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" + referencesMap_List1["A Genome-Wide Association Study Identifies Five Loci Influencing Facial Morphology in Europeans"] = "https://journals.plos.org/plosgenetics/article?id=10.1371/journal.pgen.1002932" + + lociList_1 := []int64{ //TODO: Add more SNPs. @@ -112,6 +122,13 @@ func getFacialStructureTraitObject()Trait{ 397723, } + for _, rsID := range lociList_1{ + + locusReferencesMap[rsID] = maps.Clone(referencesMap_List1) + } + + facialStructureLociList := helpers.GetListOfMapKeys(locusReferencesMap) + referencesMap := make(map[string]string) referencesMap["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" referencesMap["A Genome-Wide Association Study Identifies Five Loci Influencing Facial Morphology in Europeans"] = "https://journals.plos.org/plosgenetics/article?id=10.1371/journal.pgen.1002932" @@ -119,10 +136,13 @@ func getFacialStructureTraitObject()Trait{ facialStructureObject := Trait{ TraitName: "Facial Structure", TraitDescription: "The structure of a person's face.", + DiscreteOrNumeric: "Discrete", + LocusReferencesMap: locusReferencesMap, LociList: facialStructureLociList, + LociList_Rules: []int64{}, RulesList: []TraitRule{}, OutcomesList: []string{}, - References: referencesMap, + ReferencesMap: referencesMap, } return facialStructureObject diff --git a/resources/geneticReferences/traits/hairColor.go b/resources/geneticReferences/traits/hairColor.go index 3d6c02e..6e71b10 100644 --- a/resources/geneticReferences/traits/hairColor.go +++ b/resources/geneticReferences/traits/hairColor.go @@ -3,10 +3,20 @@ package traits // Hair color is influenced by thousands of genes // We only have a few listed here +import "seekia/internal/helpers" + +import "maps" func getHairColorTraitObject()Trait{ - hairColorLociList := []int64{ + // Map Structure: rsID -> References Map + locusReferencesMap := make(map[int64]map[string]string) + + referencesMap_List1 := make(map[string]string) + referencesMap_List1["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" + referencesMap_List1["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" + + lociList_1 := []int64{ //These loci were taken from https://pubmed.ncbi.nlm.nih.gov/20546537/ @@ -32,6 +42,13 @@ func getHairColorTraitObject()Trait{ 1805008, } + for _, rsID := range lociList_1{ + + locusReferencesMap[rsID] = maps.Clone(referencesMap_List1) + } + + hairColorLociList := helpers.GetListOfMapKeys(locusReferencesMap) + referencesMap := make(map[string]string) referencesMap["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" referencesMap["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" @@ -39,10 +56,13 @@ func getHairColorTraitObject()Trait{ hairColorObject := Trait{ TraitName: "Hair Color", TraitDescription: "The color of a person's hair.", + DiscreteOrNumeric: "Discrete", + LocusReferencesMap: locusReferencesMap, LociList: hairColorLociList, + LociList_Rules: []int64{}, RulesList: []TraitRule{}, OutcomesList: []string{}, - References: referencesMap, + ReferencesMap: referencesMap, } return hairColorObject diff --git a/resources/geneticReferences/traits/hairTexture.go b/resources/geneticReferences/traits/hairTexture.go index b487c1f..b731eda 100644 --- a/resources/geneticReferences/traits/hairTexture.go +++ b/resources/geneticReferences/traits/hairTexture.go @@ -1,11 +1,53 @@ package traits +import "seekia/internal/helpers" +import "maps" func getHairTextureTraitObject()Trait{ - rule1_ReferencesMap := make(map[string]string) - rule1_ReferencesMap["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" + // Map Structure: rsID -> References Map + locusReferencesMap := make(map[int64]map[string]string) + + referencesMap_List1 := make(map[string]string) + referencesMap_List1["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" + + lociList_1 := []int64{ + 7349332, + } + + for _, rsID := range lociList_1{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List1) + } + + + referencesMap_List2 := make(map[string]string) + referencesMap_List2["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" + + lociList_2 := []int64{ + 11803731, + } + + for _, rsID := range lociList_2{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List2) + } + + + referencesMap_List3 := make(map[string]string) + referencesMap_List3["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" + + lociList_3 := []int64{ + 17646946, + } + + for _, rsID := range lociList_3{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List3) + } + + + + referencesMap_rs7349332 := make(map[string]string) + referencesMap_rs7349332["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" rule1_Locus1Object := RuleLocus{ @@ -22,12 +64,10 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "fde405", LociList: rule1_LociList, OutcomePointsMap: rule1_OutcomePointsMap, - References: rule1_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs7349332), } //TODO: Make sure this is true, that a heterozygote has a higher likelihood of curly hair - rule2_ReferencesMap := make(map[string]string) - rule2_ReferencesMap["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" rule2_Locus1Object := RuleLocus{ @@ -44,11 +84,8 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "6bd1da", LociList: rule2_LociList, OutcomePointsMap: rule2_OutcomePointsMap, - References: rule2_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs7349332), } - - rule3_ReferencesMap := make(map[string]string) - rule3_ReferencesMap["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" rule3_Locus1Object := RuleLocus{ @@ -65,11 +102,13 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "32e377", LociList: rule3_LociList, OutcomePointsMap: rule3_OutcomePointsMap, - References: rule3_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs7349332), } - rule4_ReferencesMap := make(map[string]string) - rule4_ReferencesMap["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" + + + referencesMap_rs11803731 := make(map[string]string) + referencesMap_rs11803731["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" rule4_Locus1Object := RuleLocus{ @@ -86,12 +125,10 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "34e6d2", LociList: rule4_LociList, OutcomePointsMap: rule4_OutcomePointsMap, - References: rule4_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs11803731), } //TODO: Make sure this is true, that a heterozygote has a higher likelihood of curly hair - rule5_ReferencesMap := make(map[string]string) - rule5_ReferencesMap["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" rule5_Locus1Object := RuleLocus{ @@ -108,11 +145,8 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "cf6cb5", LociList: rule5_LociList, OutcomePointsMap: rule5_OutcomePointsMap, - References: rule5_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs11803731), } - - rule6_ReferencesMap := make(map[string]string) - rule6_ReferencesMap["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" rule6_Locus1Object := RuleLocus{ @@ -129,11 +163,11 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "2ba65b", LociList: rule6_LociList, OutcomePointsMap: rule6_OutcomePointsMap, - References: rule6_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs11803731), } - rule7_ReferencesMap := make(map[string]string) - rule7_ReferencesMap["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" + referencesMap_rs17646946 := make(map[string]string) + referencesMap_rs17646946["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" rule7_Locus1Object := RuleLocus{ @@ -150,12 +184,9 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "ae3274", LociList: rule7_LociList, OutcomePointsMap: rule7_OutcomePointsMap, - References: rule7_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs17646946), } - rule8_ReferencesMap := make(map[string]string) - rule8_ReferencesMap["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" - rule8_Locus1Object := RuleLocus{ LocusIdentifier: "f1144a", @@ -171,12 +202,9 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "a546bf", LociList: rule8_LociList, OutcomePointsMap: rule8_OutcomePointsMap, - References: rule8_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs17646946), } - rule9_ReferencesMap := make(map[string]string) - rule9_ReferencesMap["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" - rule9_Locus1Object := RuleLocus{ LocusIdentifier: "468bb3", @@ -192,26 +220,32 @@ func getHairTextureTraitObject()Trait{ RuleIdentifier: "b8dc0a", LociList: rule9_LociList, OutcomePointsMap: rule9_OutcomePointsMap, - References: rule9_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs17646946), } hairTextureRulesList := []TraitRule{rule1_Object, rule2_Object, rule3_Object, rule4_Object, rule5_Object, rule6_Object, rule7_Object, rule8_Object, rule9_Object} - hairTextureLociList := []int64{17646946, 11803731, 7349332} + + lociList_Rules := []int64{7349332, 11803731, 17646946} referencesMap := make(map[string]string) referencesMap["SNPedia.com - Hair Curliness"] = "https://www.snpedia.com/index.php/Hair_curliness" outcomesList := []string{"Straight", "Curly"} + hairTextureLociList := helpers.GetListOfMapKeys(locusReferencesMap) + hairTextureObject := Trait{ TraitName: "Hair Texture", TraitDescription: "The texture of a person's head hair.", + DiscreteOrNumeric: "Discrete", + LocusReferencesMap: locusReferencesMap, LociList: hairTextureLociList, + LociList_Rules: lociList_Rules, RulesList: hairTextureRulesList, OutcomesList: outcomesList, - References: referencesMap, + ReferencesMap: referencesMap, } return hairTextureObject diff --git a/resources/geneticReferences/traits/lactoseTolerance.go b/resources/geneticReferences/traits/lactoseTolerance.go index ca36490..807a93b 100644 --- a/resources/geneticReferences/traits/lactoseTolerance.go +++ b/resources/geneticReferences/traits/lactoseTolerance.go @@ -1,11 +1,30 @@ package traits +import "seekia/internal/helpers" + +import "maps" func getLactoseToleranceTraitObject()Trait{ - rule1_ReferencesMap := make(map[string]string) - rule1_ReferencesMap["SNPedia.com - rs182549"] = "https://www.snpedia.com/index.php/Rs182549" + // Map Structure: rsID -> References Map + locusReferencesMap := make(map[int64]map[string]string) + + referencesMap_1 := make(map[string]string) + referencesMap_1["SNPedia.com - rs182549"] = "https://www.snpedia.com/index.php/Rs182549" + + locusReferencesMap[182549] = referencesMap_1 + + + referencesMap_2 := make(map[string]string) + referencesMap_2["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" + + locusReferencesMap[4988235] = referencesMap_2 + + + + referencesMap_rs182549 := make(map[string]string) + referencesMap_rs182549["SNPedia.com - rs182549"] = "https://www.snpedia.com/index.php/Rs182549" rule1_Locus1Object := RuleLocus{ @@ -22,12 +41,9 @@ func getLactoseToleranceTraitObject()Trait{ RuleIdentifier: "f4e02c", LociList: rule1_LociList, OutcomePointsMap: rule1_OutcomePointsMap, - References: rule1_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs182549), } - rule2_ReferencesMap := make(map[string]string) - rule2_ReferencesMap["SNPedia.com - rs182549"] = "https://www.snpedia.com/index.php/Rs182549" - rule2_Locus1Object := RuleLocus{ LocusIdentifier: "a7feff", @@ -43,11 +59,11 @@ func getLactoseToleranceTraitObject()Trait{ RuleIdentifier: "cc3df0", LociList: rule2_LociList, OutcomePointsMap: rule2_OutcomePointsMap, - References: rule2_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs182549), } - rule3_ReferencesMap := make(map[string]string) - rule3_ReferencesMap["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" + referencesMap_rs4988235 := make(map[string]string) + referencesMap_rs4988235["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" rule3_Locus1Object := RuleLocus{ @@ -64,12 +80,9 @@ func getLactoseToleranceTraitObject()Trait{ RuleIdentifier: "8170ee", LociList: rule3_LociList, OutcomePointsMap: rule3_OutcomePointsMap, - References: rule3_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs4988235), } - rule4_ReferencesMap := make(map[string]string) - rule4_ReferencesMap["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" - rule4_Locus1Object := RuleLocus{ LocusIdentifier: "176dde", @@ -85,12 +98,9 @@ func getLactoseToleranceTraitObject()Trait{ RuleIdentifier: "52425f", LociList: rule4_LociList, OutcomePointsMap: rule4_OutcomePointsMap, - References: rule4_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs4988235), } - rule5_ReferencesMap := make(map[string]string) - rule5_ReferencesMap["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" - rule5_Locus1Object := RuleLocus{ LocusIdentifier: "164acb", @@ -106,25 +116,31 @@ func getLactoseToleranceTraitObject()Trait{ RuleIdentifier: "4b5c35", LociList: rule5_LociList, OutcomePointsMap: rule5_OutcomePointsMap, - References: rule5_ReferencesMap, + ReferencesMap: maps.Clone(referencesMap_rs4988235), } lactoseToleranceRulesList := []TraitRule{rule1_Object, rule2_Object, rule3_Object, rule4_Object, rule5_Object} - lactoseToleranceLociList := []int64{4988235, 182549} referencesMap := make(map[string]string) referencesMap["SNPedia.com - Lactose Intolerance"] = "https://www.snpedia.com/index.php/Lactose_intolerance" outcomesList := []string{"Tolerant", "Intolerant"} + lactoseToleranceLociList := helpers.GetListOfMapKeys(locusReferencesMap) + + lociList_Rules := []int64{182549, 4988235} + lactoseToleranceObject := Trait{ TraitName: "Lactose Tolerance", TraitDescription: "The ability to tolerate lactose.", + DiscreteOrNumeric: "Discrete", + LocusReferencesMap: locusReferencesMap, LociList: lactoseToleranceLociList, + LociList_Rules: lociList_Rules, RulesList: lactoseToleranceRulesList, OutcomesList: outcomesList, - References: referencesMap, + ReferencesMap: referencesMap, } return lactoseToleranceObject diff --git a/resources/geneticReferences/traits/skinColor.go b/resources/geneticReferences/traits/skinColor.go index 79e7af8..699b4b5 100644 --- a/resources/geneticReferences/traits/skinColor.go +++ b/resources/geneticReferences/traits/skinColor.go @@ -1,10 +1,18 @@ package traits +import "seekia/internal/helpers" +import "maps" func getSkinColorTraitObject()Trait{ - skinColorLociList := []int64{ + // Map Structure: rsID -> References Map + locusReferencesMap := make(map[int64]map[string]string) + + referencesMap_List1 := make(map[string]string) + referencesMap_List1["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" + + lociList_1 := []int64{ //TODO: Add more SNPs. @@ -14,12 +22,32 @@ func getSkinColorTraitObject()Trait{ 26722, 1426654, 642742, + } + + for _, rsID := range lociList_1{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List1) + } + + referencesMap_List2 := make(map[string]string) + referencesMap_List2["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" + + lociList_2 := []int64{ // These SNPs are from https://pubmed.ncbi.nlm.nih.gov/20546537/ 16891982, 12203592, 1042602, 1834640, + } + + for _, rsID := range lociList_2{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List2) + } + + referencesMap_List3 := make(map[string]string) + referencesMap_List3["Meta-analysis and prioritization of human skin pigmentation-associated GWAS-SNPs using ENCODE data-based web-tools"] = "https://link.springer.com/article/10.1007/s00403-019-01891-3" + + lociList_3 := []int64{ // These SNPs are from https://link.springer.com/article/10.1007/s00403-019-01891-3 7182710, @@ -33,18 +61,28 @@ func getSkinColorTraitObject()Trait{ 3212368, } + for _, rsID := range lociList_3{ + locusReferencesMap[rsID] = maps.Clone(referencesMap_List3) + } + + referencesMap := make(map[string]string) referencesMap["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" referencesMap["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" referencesMap["Meta-analysis and prioritization of human skin pigmentation-associated GWAS-SNPs using ENCODE data-based web-tools"] = "https://link.springer.com/article/10.1007/s00403-019-01891-3" + lociList := helpers.GetListOfMapKeys(locusReferencesMap) + skinColorObject := Trait{ TraitName: "Skin Color", TraitDescription: "The color of a person's skin.", - LociList: skinColorLociList, + DiscreteOrNumeric: "Discrete", + LocusReferencesMap: locusReferencesMap, + LociList: lociList, + LociList_Rules: []int64{}, RulesList: []TraitRule{}, OutcomesList: []string{}, - References: referencesMap, + ReferencesMap: referencesMap, } return skinColorObject diff --git a/resources/geneticReferences/traits/traits.go b/resources/geneticReferences/traits/traits.go index f555eec..4685994 100644 --- a/resources/geneticReferences/traits/traits.go +++ b/resources/geneticReferences/traits/traits.go @@ -3,30 +3,51 @@ package traits -// TODO: We want to eventually use neural nets for both trait and polygenic disease analysis -// These will be trained on a set of genomes and will output a probability analysis for each trait -// This is only possible once we get access to the necessary training data -// -// See geneticPrediction.go for a non-working attempt to predict traits with neural nets - import "errors" -type RuleLocus struct{ +type Trait struct{ - // 3 byte hex encoded string - LocusIdentifier string + // Example: "Eye Color" + TraitName string - // RSID that represents this locus - // If multiple RSIDs represent the same locus, use the first rsid for the locus in the locusMetadata package - LocusRSID int64 + TraitDescription string - // List of base pair values that this RSID must fulfill to pass the rule - // As long as the value matches any base pair value in the list, the genome has passed this rule locus - // The genome must pass every rule locus within a rule to pass the rule - BasePairsList []string + // This describes if the trait is discrete or numeric + // Discrete traits have a set of outcomes (Example: Eye Color: Blue, Green...) + // Numeric traits have a numeric outcome (Example: Height) + // The value of this variable is either "Discrete" or "Numeric" + DiscreteOrNumeric string + + // This is a list of rsIDs which are known to have an effect on this trait + // These loci may not have any associated rules + // We use these loci to predict trait outcomes with neural networks. + // We also use these loci to calculate Racial Similarity. + // Map Structure: rsID -> (map[ReferenceName]Reference Link) + LocusReferencesMap map[int64]map[string]string + + // This is a list of all loci used to predict this trait + // If a neural network exists, all of these will be used as input into the network for prediction + LociList []int64 + + // This is a list of all loci used to predict this trait using rules + // It is sometimes a subset of LociList + LociList_Rules []int64 + + // This list can be empty if no rules exist + // An empty list means we are relying on LociList and neural networks for trait prediction. + RulesList []TraitRule + + // List of outcomes + // Example: "Lactose Intolerant", "Lactore Tolerant" + // This list can be empty if outcomes are not text descriptions (Example: Facial structure) + // If the trait is Numeric, or their or no rules nor a neural network, then this list will be empty. + OutcomesList []string + + // This map contains scientific resources about this trait + // Map structure: Reference name -> Reference link + ReferencesMap map[string]string } - type TraitRule struct{ // 3 byte identifier encoded hex @@ -44,36 +65,26 @@ type TraitRule struct{ OutcomePointsMap map[string]int // Map structure: Reference name -> Reference link - References map[string]string + ReferencesMap map[string]string } -type Trait struct{ +type RuleLocus struct{ - TraitName string + // 3 byte hex encoded string + LocusIdentifier string - TraitDescription string + // RSID that represents this locus + // If multiple RSIDs represent the same locus, use the first rsid for the locus in the locusMetadata package + LocusRSID int64 - // This is a list of rsIDs which are known to have an effect on this trait - // These loci may not have any associated rules - // We use these loci to calculate Racial Similarity. - // We will also use neural networks to predict trait outcome scores using these loci - LociList []int64 - - // This list can be empty if no rules exist - // An empty list means we are relying on LociList and neural networks for trait prediction. - RulesList []TraitRule - - // List of outcomes - // Example: "Lactose Intolerant", "Lactore Tolerant" - // This list can be empty if outcomes are not text descriptions (Example: Facial structure) - // If there are no outcomes, then no rules can exist - OutcomesList []string - - // Map structure: Reference name -> Reference link - References map[string]string + // List of base pair values that this RSID must fulfill to pass the rule + // As long as the value matches any base pair value in the list, the genome has passed this rule locus + // The genome must pass every rule locus within a rule to pass the rule + BasePairsList []string } + var traitNamesList []string var traitObjectsList []Trait diff --git a/utilities/createGeneticModels/.gitignore b/utilities/createGeneticModels/.gitignore index fe5b4d2..8ac805f 100644 --- a/utilities/createGeneticModels/.gitignore +++ b/utilities/createGeneticModels/.gitignore @@ -1,3 +1,4 @@ OpenSNPDataArchiveFolderpath.txt TrainingData -TrainedModels \ No newline at end of file +TrainedModels +ModelAccuracies \ No newline at end of file diff --git a/utilities/createGeneticModels/createGeneticModels.go b/utilities/createGeneticModels/createGeneticModels.go index 23c2506..5e75843 100644 --- a/utilities/createGeneticModels/createGeneticModels.go +++ b/utilities/createGeneticModels/createGeneticModels.go @@ -3,6 +3,7 @@ // These are neural networks which predict traits such as eye color from raw genome files // The OpenSNP.org dataset is used, and more datasets will be added in the future. // You must download the dataset and extract it. The instructions are described in the utility. +// The trained models are saved in the /resources/geneticPredictionModels package for use in the Seekia app. package main @@ -1136,10 +1137,16 @@ func setStartAndMonitorTrainModelPage(window fyne.Window, traitName string, prev _, err := localFilesystem.CreateFolder("./TrainedModels") if (err != nil) { return false, err } - trainingSetFilepathsList, _, err := getTrainingAndTestingDataFilepathLists(traitName) if (err != nil) { return false, err } + // Now we deterministically randomize the order of the trainingSetFilepathsList + pseudorandomNumberGenerator := mathRand.New(mathRand.NewPCG(1, 2)) + + pseudorandomNumberGenerator.Shuffle(len(trainingSetFilepathsList), func(i int, j int){ + trainingSetFilepathsList[i], trainingSetFilepathsList[j] = trainingSetFilepathsList[j], trainingSetFilepathsList[i] + }) + // We create a new neural network object to train neuralNetworkObject, err := geneticPrediction.GetNewUntrainedNeuralNetworkObject(traitName) if (err != nil) { return false, err } @@ -1277,7 +1284,8 @@ func setTestModelsPage(window fyne.Window, previousPage func()){ description2 := getLabelCentered("This will test each neural network using user training data examples.") description3 := getLabelCentered("The testing data is not used to train the models.") description4 := getLabelCentered("The results of the testing will be displayed at the end.") - description5 := getLabelCentered("You must select a trait model to test.") + description5 := getLabelCentered("The results will also be saved in the ModelAccuracies folder.") + description6 := getLabelCentered("You must select a trait model to test.") traitNamesList := []string{"Eye Color", "Lactose Tolerance"} @@ -1301,50 +1309,12 @@ func setTestModelsPage(window fyne.Window, previousPage func()){ traitNameSelectorCentered := getWidgetCentered(traitNameSelector) - page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), traitNameSelectorCentered, widget.NewSeparator(), beginTestingButton) + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, widget.NewSeparator(), traitNameSelectorCentered, widget.NewSeparator(), beginTestingButton) window.SetContent(page) } -type TraitOutcomeInfo struct{ - - // This is the outcome which was found - // Example: "Blue" - OutcomeName string - - // This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction - PercentageOfLociTested int - - // This is a value between 0-100 which describes the percentage of the tested loci which were phased for the input for the prediction - PercentageOfPhasedLoci int -} - -type TraitPredictionAccuracyInfo struct{ - - // This contains the quantity of examples for the outcome with the specified percentageOfLociTested and percentageOfPhasedLoci - QuantityOfExamples int - - // This contains the quantity of predictions for the outcome with the specified percentageOfLociTested and percentageOfPhasedLoci - // Prediction = our model predicted this outcome - QuantityOfPredictions int - - // This stores the probability (0-100) that our model will accurately predict this outcome for a genome which has - // the specified percentageOfLociTested and percentageOfPhasedLoci - // In other words: What is the probability that if you give Seekia a blue-eyed genome, it will give you a correct Blue prediction? - // This value is only accurate is QuantityOfExamples > 0 - ProbabilityOfCorrectGenomePrediction int - - // This stores the probability (0-100) that our model is correct if our model predicts that a genome - // with the specified percentageOfLociTested and percentageOfPhasedLoci has this outcome - // In other words: What is the probability that if Seekia says a genome will have blue eyes, it is correct? - // This value is only accurate is QuantityOfPredictions > 0 - ProbabilityOfCorrectOutcomePrediction int -} - -// Map Structure: Trait Outcome Info -> Trait Prediction Accuracy Info -type TraitAccuracyInfoMap map[TraitOutcomeInfo]TraitPredictionAccuracyInfo - func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previousPage func()){ title := getBoldLabelCentered("Testing Model") @@ -1386,9 +1356,9 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ //Outputs: // -bool: Process completed (true == was not stopped mid-way) - // -TraitAccuracyInfoMap + // -geneticPrediction.TraitPredictionAccuracyInfoMap // -error - testModel := func()(bool, TraitAccuracyInfoMap, error){ + testModel := func()(bool, geneticPrediction.TraitPredictionAccuracyInfoMap, error){ type TraitAccuracyStatisticsValue struct{ @@ -1408,7 +1378,7 @@ 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 - traitPredictionInfoMap := make(map[TraitOutcomeInfo]TraitAccuracyStatisticsValue) + traitPredictionInfoMap := make(map[geneticPrediction.TraitOutcomeInfo]TraitAccuracyStatisticsValue) _, testingSetFilepathsList, err := getTrainingAndTestingDataFilepathLists(traitName) @@ -1494,7 +1464,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ { // We first add the information to the map for the correct outcome - newTraitOutcomeInfo_CorrectOutcome := TraitOutcomeInfo{ + newTraitOutcomeInfo_CorrectOutcome := geneticPrediction.TraitOutcomeInfo{ OutcomeName: correctOutcomeName, PercentageOfLociTested: percentageOfLociTested, @@ -1525,7 +1495,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ { // We now add the information to the map for the predicted outcome - newTraitOutcomeInfo_PredictedOutcome := TraitOutcomeInfo{ + newTraitOutcomeInfo_PredictedOutcome := geneticPrediction.TraitOutcomeInfo{ OutcomeName: predictedOutcomeName, PercentageOfLociTested: percentageOfLociTested, @@ -1566,7 +1536,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ // Now we construct the TraitAccuracyInfoMap // This map stores the accuracy for each outcome - traitAccuracyInfoMap := make(map[TraitOutcomeInfo]TraitPredictionAccuracyInfo) + traitPredictionAccuracyInfoMap := make(map[geneticPrediction.TraitOutcomeInfo]geneticPrediction.TraitPredictionAccuracyInfo) for traitAccuracyData, value := range traitPredictionInfoMap{ @@ -1583,7 +1553,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ return false, nil, errors.New("traitPredictionInfoMap contains quantityOfCorrectOutcomePredictions > quantityOfPredictions") } - newTraitPredictionAccuracyInfo := TraitPredictionAccuracyInfo{ + newTraitPredictionAccuracyInfo := geneticPrediction.TraitPredictionAccuracyInfo{ QuantityOfExamples: quantityOfExamples, QuantityOfPredictions: quantityOfPredictions, } @@ -1604,17 +1574,30 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ newTraitPredictionAccuracyInfo.ProbabilityOfCorrectOutcomePrediction = percentageOfCorrectOutcomePredictions } - traitAccuracyInfoMap[traitAccuracyData] = newTraitPredictionAccuracyInfo + traitPredictionAccuracyInfoMap[traitAccuracyData] = newTraitPredictionAccuracyInfo } // Testing is complete. + // We save the info map as a file in the ModelAccuracies folder + + fileBytes, err := geneticPrediction.EncodeTraitPredictionAccuracyInfoMapToBytes(traitPredictionAccuracyInfoMap) + if (err != nil) { return false, nil, err } + + _, err = localFilesystem.CreateFolder("./ModelAccuracies") + if (err != nil) { return false, nil, err } + + modelAccuracyFilename := traitNameWithoutWhitespaces + "ModelAccuracy.gob" + + err = localFilesystem.CreateOrOverwriteFile(fileBytes, "./ModelAccuracies/", modelAccuracyFilename) + if (err != nil) { return false, nil, err } + progressPercentageBinding.Set(1) - return true, traitAccuracyInfoMap, nil + return true, traitPredictionAccuracyInfoMap, nil } - processIsComplete, traitAccuracyInfoMap, err := testModel() + processIsComplete, traitPredictionAccuracyInfoMap, err := testModel() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return @@ -1624,14 +1607,14 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ return } - setViewModelTestingTraitResultsPage(window, traitName, traitAccuracyInfoMap, previousPage) + setViewModelTestingTraitResultsPage(window, traitName, traitPredictionAccuracyInfoMap, previousPage) } go testModelFunction() } // This is a page to view the details of testing for a specific trait's model -func setViewModelTestingTraitResultsPage(window fyne.Window, traitName string, traitAccuracyInfoMap TraitAccuracyInfoMap, exitPage func()){ +func setViewModelTestingTraitResultsPage(window fyne.Window, traitName string, traitAccuracyInfoMap geneticPrediction.TraitPredictionAccuracyInfoMap, exitPage func()){ title := getBoldLabelCentered("Trait Prediction Accuracy Details") @@ -1661,14 +1644,10 @@ func setViewModelTestingTraitResultsPage(window fyne.Window, traitName string, t predictionAccuracyTitle3 := getItalicLabelCentered("Prediction Accuracy") knownLociLabel_67to100 := getItalicLabelCentered("67-100% Known Loci") - emptyLabel2 := widget.NewLabel("") - emptyLabel3 := widget.NewLabel("") - outcomeNameColumn := container.NewVBox(outcomeNameTitle, emptyLabel1, 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()) - viewTraitAccuracyDetailsColumn := container.NewVBox(emptyLabel2, emptyLabel3, widget.NewSeparator()) traitObject, err := traits.GetTraitObject(traitName) if (err != nil) { return nil, err } @@ -1777,7 +1756,7 @@ func setViewModelTestingTraitResultsPage(window fyne.Window, traitName string, t predictionAccuracyColumn_67to100.Add(widget.NewSeparator()) } - resultsGrid := container.NewHBox(layout.NewSpacer(), outcomeNameColumn, predictionAccuracyColumn_0to33, predictionAccuracyColumn_34to66, predictionAccuracyColumn_67to100, viewTraitAccuracyDetailsColumn, layout.NewSpacer()) + resultsGrid := container.NewHBox(layout.NewSpacer(), outcomeNameColumn, predictionAccuracyColumn_0to33, predictionAccuracyColumn_34to66, predictionAccuracyColumn_67to100, layout.NewSpacer()) return resultsGrid, nil }