From 21082cfb0dae08e20d0d0b745578297d18e9fbff Mon Sep 17 00:00:00 2001 From: Simon Sarasova Date: Fri, 19 Jul 2024 17:16:28 +0000 Subject: [PATCH] Added neural network trait prediction to genetic analyses. --- Changelog.md | 1 + Contributors.md | 2 +- gui/buildProfileGui_Lifestyle.go | 2 +- gui/helpGui.go | 156 ++-- gui/viewAnalysisGui_Couple.go | 692 +++++++++++++----- gui/viewAnalysisGui_Person.go | 504 ++++++++----- gui/viewGeneticReferencesGui.go | 10 +- gui/viewProfileGui.go | 655 ++++++++++------- internal/generate/generate.go | 2 +- .../createCoupleGeneticAnalysis.go | 603 ++++++++------- .../createPersonGeneticAnalysis.go | 386 +++++++--- .../geneticAnalysis/geneticAnalysis.go | 245 +++++-- .../geneticPrediction/geneticPrediction.go | 267 ++++++- .../myChosenAnalysis/myChosenAnalysis.go | 4 +- internal/genetics/myGenomes/myGenomes.go | 10 +- .../prepareRawGenomes/prepareRawGenomes.go | 220 +++--- .../readGeneticAnalysis.go | 322 +++++--- .../genetics/readRawGenomes/readRawGenomes.go | 173 ++--- .../SampleCoupleAnalysis.messagepack | Bin 12014 -> 14002 bytes .../SamplePerson1Analysis.messagepack | Bin 12673 -> 14646 bytes .../SamplePerson2Analysis.messagepack | Bin 10668 -> 12540 bytes .../calculatedAttributes.go | 23 +- .../myProfileExports/myProfileExports.go | 48 +- .../geneticPredictionModels.go | 58 ++ .../geneticPredictionModels_test.go | 48 ++ .../EyeColorModelAccuracy.gob | Bin 0 -> 57054 bytes .../LactoseToleranceModelAccuracy.gob | Bin 0 -> 574 bytes .../predictionModels/EyeColorModel.gob | Bin 0 -> 196395 bytes .../LactoseToleranceModel.gob | Bin 0 -> 487 bytes .../geneticReferences_test.go | 61 +- .../polygenicDiseases/polygenicDiseases.go | 5 +- .../geneticReferences/traits/eyeColor.go | 50 +- .../traits/facialStructure.go | 24 +- .../geneticReferences/traits/hairColor.go | 24 +- .../geneticReferences/traits/hairTexture.go | 100 ++- .../traits/lactoseTolerance.go | 56 +- .../geneticReferences/traits/skinColor.go | 44 +- resources/geneticReferences/traits/traits.go | 89 ++- utilities/createGeneticModels/.gitignore | 3 +- .../createGeneticModels.go | 95 +-- 40 files changed, 3316 insertions(+), 1666 deletions(-) create mode 100644 resources/geneticPredictionModels/geneticPredictionModels.go create mode 100644 resources/geneticPredictionModels/geneticPredictionModels_test.go create mode 100644 resources/geneticPredictionModels/predictionModelAccuracies/EyeColorModelAccuracy.gob create mode 100644 resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob create mode 100644 resources/geneticPredictionModels/predictionModels/EyeColorModel.gob create mode 100644 resources/geneticPredictionModels/predictionModels/LactoseToleranceModel.gob 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 2de4ccbf7f937eafb9292f8219d5a5b1b39fb07d..558c216a14c9e99a5ad6d8f5371614ef8918da62 100644 GIT binary patch literal 14002 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 ND_McF+`hSb^52+s>T3W1 literal 12673 zcmb`Ndvp}l9mi+05E>v|BBlsSVCCT>p=eT|h^3y*k{4Obl0;~!T4#4B$(&4PaAvlf zO=~M5L>{eL&Jh)ID%M6Jfn*ISNFOT6Dxw|%X%EHP0HQS3QzarTMWCJR+{Dc8ufNXR znm_jJ&dg`Nzt`ORyLYm_(=D<>MB$YcoUHJYX#1^O5X#wb5hqG@TuG1GlA{W#FrJ!EBuII${Wug!%5d%Uf$W_#^^3eG&ZWx=uB>bJ2H3${d z$03g-RtbDSRT2$J=DL?jqEyX^d|(N$aIC^9%h=V8T@`$wR^VJ7P7qvfk*nj`#(jxb zmWWl7cJZ2IznY^y&1p$2W)*VRp1pU5>inEsR<(kac~(@FN={L^psj1@xK|4BiN}03 znnA(AGt7yZK3Vd!eqP|!NLiJSRTN&Vc9Ywan=+k4cOSOR?(1KGMZcQYllqkAlPsdOM;~7zUIz16Z7{SC~ z^f6g6`#jeH>*0vm_g^|ngQe}4Xs}l&uA;$Krbd&n-o#Y*CZsxMxBYT^681kUBxdh< z=rI~>V9VV!*u>)<5?d4ffBc=h9$>pPZ(_ zPXF$68tnMjZ?%pkPKn(|@AB*=r(akf~CXC~4@i{(z>C=_*A9>`5j(D$o4ny1{5x0)yW=+J9 zqLE0EjxhOiF$6N~`l}yWMV#SlFvK&`Fi+et+k|6JVhH5EGyGKyfqd~!dJjVY!x-lS zhcE;-Y#N3@hUI3B!4S}}aKtKdOK!bY#2Id|ia5^y-73N~Pr(ovpCvCtG$QNfA%?pI2%s+yuK)!gdvz(Xc=9%VIc9!!>ZACzmIi`JZj3i^J zHYmpIvtPfwwr%kIN{+9tQJH92FsPKXDle5NrJ@uTdv^{WdE<_A`r;PxD+#GTZlv>|4% zJ^D^>%$|29+@ozloqb#T-jSzjpRI*2X-BTSKU(`dEc{yg%z5^E+UE=Jj!oKxix#p0 zo)ui0W+*@(Zzdt$eN49AyH8I$jKub77?4x_o79F@wcGQo52^dK5SJ^VJc9>QZu$; z2$ae>*)oP03?r!=`|0Bt0`Zw6`64IIEEOK zM=~+}Nwh=^2r2i>Sf6PXGHu^H@PTQBsQhL@F@_jwmy9VHx#l-qiOS9UzJjShc}c%A zfFTCMNRV@uv|@;{cJ+d}c3}~QK!$C(l!YM%!-hs--$4w4413}xw2}=f#QoT7k62Za z?@h%JLs^m{p@^kFg$~>hvhC1R(}s~CzcPLYhCnLC z{eQEH)ZEHr2vn9!2P`kx>V7dAQ!(U)xIg1Qbd3iYRu|rcsX!5POxcYgkV@>6n=k}Y zan>KTiWGln!w@K9FWSCi8X?6mW}{P*L4_chKbdY?g`{%w{rMOI1vz8UomLTNJvxg) zD&I=4##9XM6JK(cbYKWHjurQ#3pN0;*q;408YFoHPsp3}YTEY{w8o z#7M#1X1z1M?Z+1}6@y`9-XE8BmuZBgGBf{G(+E-78@gf@d1<4ChJ?1wZHsTAeyM%# zMAGNBJ28);vSL=FQ;H#bWUA*MMS}#?J44Y>6!)LOd@&R+_0W|vWpXX10=Z8yaZ${u z6iXK8M)VCX6fuf6e^K4Pu!up~?|lX>7^o~M`l;t1MpYp9CpA8U`2rb6(M>xy9>!FR zhEWeH6}7zhWiFmK=|c)rgYFU=jn@&16>8dBd(DSPR~dKFWfjT30c|=UUvft+$5f#4 z!TLIwqJv;4o?J(Nn`vLji0ZuhsAA*+HCAM}d1_+JAezx%iy+UU1FokF~de<@w*& SzL(xTU;F;qsX0k+1pfmas;~(F diff --git a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack index bf99e654a77c1a1ebb31ec185f72c6ca2aa8789a..dc32f26715948b0b5fa0749f8f72ea653b7555a8 100644 GIT binary patch literal 12540 zcmeI2e{dA#8OQf>M`HM4KxBeBfkePxq(Gulnhw*sTmlI>ASO4^BHC{57P6DgZrt7L z<(SSW0fJx^WkwX*saQlMfH~m!VbK=u4xw6qAkJj4H29OoI<-XYF9fD@yA8k%3$XHa$;lSuxPTZsER@2}9S;oU?M5NAm@YYNNJMG$enl zqa6JyStu$2=_zmGpHlo!5_xR)4i@50MchfG=+fI(k+SB0vyeHdR^}wFgg4y4LLe=p z%H~r@bWr(NOXGBzLWqdLj)ho=D^-gtp~Z2$XgUjltQ2(oiG@IUi639jLZFB>2Op#m zB4UIVoR{3!_p=bf3J!8;?Pdxgf(&WV@5!c+w2m!lDWVX<3ZW&L7on=>FDwMei{sw) zKV%_*7RS*tI%0%l03sZol1o`hC*QH{#-}NSumWfiiY|T3LWop)lT|sqW-$u^wAhB0 z<*^VbFV0JkSVXd4ZTvF}f#i>gce4=CvEbD#1duNr`{Q3Jgh(Y3mCz(NPb>Vhea!b^Jj0d5GZ2uBUe!fA)i2!GmInjp5;_J*S$$uA>;!Q6OIml zl0pc_fV>FtzuinBgcT^r4eyju2w??~FHG3fX%)$0VpVJxINwO3XrXBeTqfI`waW*br2z(Rl^XBAx6#6k%9$!3vLG35ygA>dF*MjdZoa^485W+Ec>KIERj{D7503hf&Ob^R3V#QaHT=DH#eh&*F ztl*I>G}0?RLJKe~9gpWTpItyDw(3} zh)pYDmKYE*A-`=aWhJfryUuu62r(@2twPB-=EE*Xe)F$}u&h8@Jo#%`2oW)FWS*vK z%Oq9yRmg^f-k%ugy-QoCJ7k!$uW+{9plj%DQriLgW7X-#*|qUFMi2`J>WUV zgPqO3jR(tpLF2);bYKJ&XSPs6=WtB}Dh<6}!rH)T_-xx2XCgz3l+~ zp~1WJN>KXP?RB!zRPWPt$+n{}B3GK{*tmV~hnrV+^j%*h$;%o|A-urvH|j)F)~byf zRST-I?N`pc`h#=HZ6I;`gTu0Tu-lJ)#Djf$dLj?jwf1@*?DH*;^I*rjm#1P`>_~pO zdi=pVKUp2OpA^=lVwomj>bp1bU>9n3@L>PiGnogQn^Vk#<>XD`!B%xV&4ayiUgyCk zzjA>GEB*Ny9?W&b&x0-g`v*K&%zK^(d%HnP#cb>f_-t0o)~=zm;`a5+xXSb=uX9zv zs$U#SbuzPhqubeTv@Y@Kk|}w0Q8xQlRqY*BqO2FXHAU0gb|y{_K2Nvroi7D+QJF89 zK}~PMJ@OE7^739kd~qLmaaFh$ZP!Cv?FZmN6?@K1IiM`Y8fUa8d#=$v_qvhozoG*4gk^u(W^wde`Z*|$(mo-Z}g?`^Yxhca{U^iXts z@xvkLchiY?(3#Q?g6Q}SWgj}8^2}YC=WUxiperFbX}M6+>~zw?vqhgQDuwl?9`JqJ z5*LLEE1F4ReNBC3du0eMSNYrdS2M3|E7#E)44l-!_E-!~YWd%=o*tebXqI%@cjfNt G{{H~iyu(re literal 10668 zcmchcc}x^{6vubjip2{l8kP0HqOAwDLe*Lt(=00jvI>QbwqEV9JII7#hR)1(S<b8O{8j@s7PfQXGV=JeS8Z z4oTD+(w?iC#B=;?nqwT}8Ih($TAV}`d8>06M*&O6T4|PzwQzJHLwR>=uT1Cic=_VF z?fqKd%aWbuxim^dclGa@>?+Kqh0MGul)zA&B-&|FqMgR-mg6?w!Dx?V<;w;+o6i`| z#AOM5E|tr$jO5A8%c4Y);bvRV?a@sQRV{aKwat{(FtxNQS-ww#Yz;c?4<1Ntb6}e+ z;u6zEMbW>#C4#{f5&czGk0>#Y*a=Loz{~6X-vlED6O8EAV|s&;s0~KFAtqq*zqe9< zI0B|yVJx9g3btg8lN{K#9a&__O>c^MJ4j~ZxE8&7D-i*u+b+VMCFTBK|1?>4jHi^_uWrHh_$U@ zNX6EB6NIP?YgGZ)1*h_S0vKc*V(SeC8He~*JPrARTT0)GI0(VHZ|gf0LU4xJdR^3u z41916LU6vA-Tn0<3H_oW1eeKXcL{{x+&3hTfDoK7h6OPpBPf&2?rso*GtAbj3_@^* zCG_hEA!?N^*^F3_cvultY$J8gK61UuR!kYI~D zhml~3@1G~Z=6!US1RH$x9tk$;@D&oQuB+=JqWwVKV+j)6STeVPk_q0DQkzhB@#gSltR}Ut^-aJNBf@{F;erJp#?i^ltbc0I@Zp!w9q_?dbbA80^{e-mUleaE|x|wemZF8PcNF- zzUv7UdR1>|nWg$=K!|FXNcCm9B!~QNF4!J|uevJIIZ1Iv3fqjFlAfqh-kdeRW}?NQ z*|PI;VrI-3?GKplT!@!He;9YOHei}tbVL50o%}|vHfmebC`O32)5VfYAdFIkCQ7Bj zC>gt^CsS-^DvatAqr z1t-tS(M-JLmcQ>g*Gc{yb@i0|d7S-1K0f^O>GJpOiIV)eal*IlV(ptiIT(tKl{H-s z@^~9G@vb-YP{JpU1CtClvJHqh4uubg9R3NMzSR*|QlV2)B&4Fse)J>}Qvp8WsD~-V zeNWjMNCk%oRe$P5Qr_MQAsF`!2Wmnea#6kb_{F7=3eK0xl35UfGc3-V5Hf=H+w}7` z2*E97IX@FZa5)w~On?xT3VMw0pKu*QaPH@xO@a`dN|%gj5Q1Bm;bX@@2+pv`*Q-KC z?Cpx$9Bd7nqpkc~QatkiPI#6V$eLsOG8C`u!-HMchHO1b_Y!ZEUZm?<@a+UHtG+S5 z2C3i{?_ihB5Q0mQFUO=o2u>v`VKs!{RLsREAq3|>BQAjuoQg0jTqn|T{P0x}qDpsE zQ)Uzmgb8yhBjV-R8$B` zn85EtAOx2%6C1N3M5TiEOWHMK$O!VK$JkT|!9}dRsUL*k5~k<71N0(+Bg-HJXITD7 z210N_9=iKU2*IiBa@~gzoJ!9Cn3kBOG#d9$)~izZNe2kQtxHrAsG@QoCCtRe&<7oq z{DtntkYOtK5z^95hf~qt l+zb)Nc0q=zvK_S@f+`f*Pywmnd@(;*4k4-{Y$46Ve*p^ptwI0* 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 0000000000000000000000000000000000000000..dd8cb7bf785502a5868e6d0572843835a2239ad3 GIT binary patch literal 57054 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 Kh3W0BUH=ar_4e)n literal 0 HcmV?d00001 diff --git a/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob b/resources/geneticPredictionModels/predictionModelAccuracies/LactoseToleranceModelAccuracy.gob new file mode 100644 index 0000000000000000000000000000000000000000..6672f7500c81b5c397d43b6a2dc84fea82ef8025 GIT binary patch literal 574 zcmZvZy-or_6ou!=CYr@U{0ow3EY!-vlG2|TL!z)GJb=S6=qAf98HNNZ5iKov080x) zVJs~zEO;cV8D}S$-8HuNe&@_NbC2|c1MD;e^`$3x%V*M$!V|{&g0f*HY)4nL3;jr- zN6=$J>UTLT=n+8<`C4DHsKcwY!?8@T>2bk$DCrHawJ#&)H+WC-7MwpT@0Rv>D+bJp z5IxcNvFDh;!D_uvL+Q(5t$jYAoo>K;v3?^NCn@b_&l6FTHvPbA&LSZ=lNUTRxe_wA z*#8flH^`8LC>)r^E|wZmz=g>Jg$Y^&YO4!X+J%0Do)Wl~P$tA7PR%d9XUDN9H#Pb( zT$D)4QqQbo;R|a08~OwKIV+t5ivg5VMiu%gEmyGbDO^J>tiZtWk&?uEW?n9SLT#7r j@-=JzTPSZR*E5&3_US^_z!{`H<1&RJ)Y`Gl`$B#I=z+wD literal 0 HcmV?d00001 diff --git a/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob b/resources/geneticPredictionModels/predictionModels/EyeColorModel.gob new file mode 100644 index 0000000000000000000000000000000000000000..414c2d17d077eafb0ddba9ea476e3b8f3e8094f9 GIT binary patch literal 196395 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 TPMt*l@Zj-ADT@^f;a;1r6hOW|43r>_gR8v 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 G53lJ*Tx; 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 literal 0 HcmV?d00001 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 }