From b6f5612bbccba89732327558bf32f9d8aa9e9eb5 Mon Sep 17 00:00:00 2001 From: Simon Sarasova Date: Wed, 14 Aug 2024 11:04:19 +0000 Subject: [PATCH] Added new genetic attributes to the calculatedAttributes package. Added the ability to view and sort by these attributes in the GUI. --- Changelog.md | 1 + Contributors.md | 2 +- gui/matchesGui.go | 73 ++- internal/myMatches/myMatches.go | 2 +- .../attributeDisplay/attributeDisplay.go | 274 ++++++++-- .../calculatedAttributes.go | 515 +++++++++++++++++- .../calculatedAttributes_test.go | 2 +- 7 files changed, 773 insertions(+), 96 deletions(-) diff --git a/Changelog.md b/Changelog.md index 4b42bc3..0b2b54a 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 new genetic attributes to the calculatedAttributes package. Added the ability to view and sort by these attributes in the GUI. - *Simon Sarasova* * Upgraded Golang to version 1.23. - *Simon Sarasova* * Added the Obesity disease to genetic analyses. - *Simon Sarasova* * Implemented neural network prediction for polygenic diseases to replace old method. Added autism and homosexualness to genetic analyses. - *Simon Sarasova* diff --git a/Contributors.md b/Contributors.md index 998422b..f733603 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 | 280 \ No newline at end of file +Simon Sarasova | June 13, 2023 | 281 \ No newline at end of file diff --git a/gui/matchesGui.go b/gui/matchesGui.go index f15e5a7..f985ca4 100644 --- a/gui/matchesGui.go +++ b/gui/matchesGui.go @@ -1024,23 +1024,44 @@ func setSelectMatchesSortByAttributePage(window fyne.Window, previousPage func() err = addAttributeSelectButton("Physical", "OffspringTotalPolygenicDiseaseRiskScore", "Ascending") if (err != nil) { return nil, err } - offspringLactoseToleranceProbabilityButton := widget.NewButton("Offspring Lactose Tolerance Probability", func(){ - //TODO - showUnderConstructionDialog(window) - }) - physicalAttributeButtonsGrid.Add(offspringLactoseToleranceProbabilityButton) + err = addAttributeSelectButton("Physical", "AutismRiskScore", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringAutismRiskScore", "Ascending") + if (err != nil) { return nil, err } - offspringCurlyHairProbabilityButton := widget.NewButton("Offspring Curly Hair Probability", func(){ - //TODO - showUnderConstructionDialog(window) - }) - physicalAttributeButtonsGrid.Add(offspringCurlyHairProbabilityButton) + err = addAttributeSelectButton("Physical", "ObesityRiskScore", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringObesityRiskScore", "Ascending") + if (err != nil) { return nil, err } - offspringStraightHairProbabilityButton := widget.NewButton("Offspring Straight Hair Probability", func(){ - //TODO - showUnderConstructionDialog(window) - }) - physicalAttributeButtonsGrid.Add(offspringStraightHairProbabilityButton) + err = addAttributeSelectButton("Physical", "OffspringBlueEyesProbability", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringGreenEyesProbability", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringHazelEyesProbability", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringBrownEyesProbability", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "OffspringLactoseToleranceProbability", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "OffspringStraightHairProbability", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringCurlyHairProbability", "Descending") + if (err != nil) { return nil, err } + + // Numeric Traits: + + err = addAttributeSelectButton("Physical", "HomosexualnessScore", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringHomosexualnessScore", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "PredictedHeight", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "OffspringPredictedHeight", "Descending") + if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "23andMe_NeanderthalVariants", "Descending") if (err != nil) { return nil, err } @@ -1555,7 +1576,6 @@ func setAddAttributeToCustomMatchDisplayPage(window fyne.Window, previousPage fu "23andMe_MaternalHaplogroup", "23andMe_PaternalHaplogroup", "23andMe_NeanderthalVariants", - "OffspringProbabilityOfAnyMonogenicDisease", "EyeColorSimilarity", "EyeColorGenesSimilarity", "HairColorSimilarity", @@ -1568,6 +1588,27 @@ func setAddAttributeToCustomMatchDisplayPage(window fyne.Window, previousPage fu "23andMe_AncestralSimilarity", "23andMe_MaternalHaplogroupSimilarity", "23andMe_PaternalHaplogroupSimilarity", + "OffspringProbabilityOfAnyMonogenicDisease", + "TotalPolygenicDiseaseRiskScore", + "OffspringTotalPolygenicDiseaseRiskScore", + "AutismRiskScore", + "OffspringAutismRiskScore", + "ObesityRiskScore", + "OffspringObesityRiskScore", + "PredictedEyeColor", + "OffspringBlueEyesProbability", + "OffspringGreenEyesProbability", + "OffspringHazelEyesProbability", + "OffspringBrownEyesProbability", + "PredictedLactoseTolerance", + "OffspringLactoseToleranceProbability", + "PredictedHairTexture", + "OffspringStraightHairProbability", + "OffspringCurlyHairProbability", + "HomosexualnessScore", + "OffspringHomosexualnessScore", + "PredictedHeight", + "OffspringPredictedHeight", } lifestyleAttributeNamesList := []string{ diff --git a/internal/myMatches/myMatches.go b/internal/myMatches/myMatches.go index 10f7cef..0bf6d3b 100644 --- a/internal/myMatches/myMatches.go +++ b/internal/myMatches/myMatches.go @@ -449,7 +449,7 @@ func StartUpdatingMyMatches(networkType byte)error{ return nil } - newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 80) + newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 95) if (err != nil) { return err } newProgressFloat := float64(newScaledPercentageInt)/100 diff --git a/internal/profiles/attributeDisplay/attributeDisplay.go b/internal/profiles/attributeDisplay/attributeDisplay.go index 49b9ae2..4d6b0ee 100644 --- a/internal/profiles/attributeDisplay/attributeDisplay.go +++ b/internal/profiles/attributeDisplay/attributeDisplay.go @@ -5,7 +5,7 @@ package attributeDisplay //TODO: Deal with singular/multiple values and how that changes an attribute value's units -// For example: 1 variant, 2 variants +// For example: 1 variant, 2 variants, 1 centimeter, 2 centimeters import "seekia/resources/worldLocations" import "seekia/resources/worldLanguages" @@ -43,6 +43,7 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str } formatPercentageFunction := func(input string)(string, error){ + valueFloat64, err := helpers.ConvertStringToFloat64(input) if (err != nil){ return "", errors.New("formatPercentageFunction called with non-numeric " + attributeName + " value: " + input) @@ -52,7 +53,8 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return "", errors.New("formatPercentageFunction called with invalid " + attributeName + " percentage value: " + input) } - result := input + "%" + result := helpers.ConvertIntToString(int(valueFloat64)) + return result, nil } @@ -197,24 +199,6 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil } - case "CatsRating":{ - - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Cats Rating") - - return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil - } - case "DogsRating":{ - - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Dogs Rating") - - return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil - } - case "PetsRating":{ - - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Pets Rating") - - return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil - } case "GenderIdentity":{ titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Gender Identity") @@ -231,20 +215,23 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return titleTranslated, false, translateGenderFunction, "", noResponseTranslated, nil } case "FruitRating", - "VegetablesRating", - "NutsRating", - "GrainsRating", - "DairyRating", - "SeafoodRating", - "BeefRating", - "PorkRating", - "PoultryRating", - "EggsRating", - "BeansRating":{ + "VegetablesRating", + "NutsRating", + "GrainsRating", + "DairyRating", + "SeafoodRating", + "BeefRating", + "PorkRating", + "PoultryRating", + "EggsRating", + "BeansRating", + "PetsRating", + "DogsRating", + "CatsRating":{ - foodName := strings.TrimSuffix(attributeName, "Rating") + thingName := strings.TrimSuffix(attributeName, "Rating") - attributeTitle := foodName + " Rating" + attributeTitle := thingName + " Rating" titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(attributeTitle) @@ -422,14 +409,92 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return titleTranslated, true, passValueFunction, "/4", noResponseTranslated, nil } - case "Height":{ + case "Height", + "PredictedHeight", + "OffspringPredictedHeight":{ - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Height") + getAttributeTitle := func()string{ + + switch attributeName{ + case "Height":{ + return "Height" + } + case "PredictedHeight":{ + return "Predicted Height" + } + } + + return "Offspring Predicted Height" + } + + attributeTitle := getAttributeTitle() + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(attributeTitle) + + getMyMetricOrImperial := func()(string, error){ + + exists, metricOrImperial, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil) { return "", err } + if (exists == false){ + return "Metric", nil + } + if (metricOrImperial != "Metric" && metricOrImperial != "Imperial"){ + return "", errors.New("Malformed globalSettings: Invalid metricOrImperial: " + metricOrImperial) + } + + return metricOrImperial, nil + } + + myMetricOrImperial, err := getMyMetricOrImperial() + if (err != nil) { return "", false, nil, "", "", err } + + formatHeightFunction := func(input string)(string, error){ + + inputCentimeters, err := helpers.ConvertStringToFloat64(input) + if (err != nil) { return "", err } + + if (myMetricOrImperial == "Metric"){ + + centimetersString := helpers.ConvertFloat64ToStringRounded(inputCentimeters, 2) + + return centimetersString, nil + } + + feetInchesString, err := helpers.ConvertCentimetersToFeetInchesTranslatedString(inputCentimeters) + if (err != nil) { return "", err } + + return feetInchesString, nil + } + + getUnitsTranslated := func()string{ + + if (myMetricOrImperial == "Metric"){ + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Centimeters") + + return unitsTranslated + } + + // There are no units to add + // The value is in the format "5 feet, 10 inches" + + return "" + } + + unitsTranslated := getUnitsTranslated() - unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("centimeters") unitsWithPadding := " " + unitsTranslated - return titleTranslated, true, roundNumberFunction, unitsWithPadding, noResponseTranslated, nil + getUnavailableText := func()string{ + if (attributeName == "Height"){ + return noResponseTranslated + } + return unknownTranslated + } + + unavailableText := getUnavailableText() + + return titleTranslated, true, formatHeightFunction, unitsWithPadding, unavailableText, nil } case "Sex":{ @@ -452,24 +517,6 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return titleTranslated, false, translateValueFunction, "", unknownTranslated, nil } - case "OffspringProbabilityOfAnyMonogenicDisease":{ - - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Probability Of Any Monogenic Disease") - - return titleTranslated, true, formatPercentageFunction, "", unknownTranslated, nil - } - case "TotalPolygenicDiseaseRiskScore":{ - - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Total Polygenic Disease Risk Score") - - return titleTranslated, true, passValueFunction, "/100", noResponseTranslated, nil - } - case "OffspringTotalPolygenicDiseaseRiskScore":{ - - titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Total Polygenic Disease Risk Score") - - return titleTranslated, true, passValueFunction, "/100", unknownTranslated, nil - } case "23andMe_AncestryComposition":{ // There is no way to display this as text, we use the gui instead @@ -867,6 +914,24 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil } + case "OffspringProbabilityOfAnyMonogenicDisease":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Probability Of Any Monogenic Disease") + + return titleTranslated, true, formatPercentageFunction, "%", unknownTranslated, nil + } + case "TotalPolygenicDiseaseRiskScore":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Total Polygenic Disease Risk Score") + + return titleTranslated, true, passValueFunction, "/100", noResponseTranslated, nil + } + case "OffspringTotalPolygenicDiseaseRiskScore":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Total Polygenic Disease Risk Score") + + return titleTranslated, true, passValueFunction, "/100", unknownTranslated, nil + } case "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested":{ titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Probability Of Any Monogenic Disease - Number Of Diseases Tested") @@ -897,6 +962,107 @@ func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(str return titleTranslated, true, passValueFunction, unitsWithPadding, "", nil } + case "AutismRiskScore", + "OffspringAutismRiskScore", + "ObesityRiskScore", + "OffspringObesityRiskScore", + "HomosexualnessScore", + "OffspringHomosexualnessScore":{ + + getAttributeTitle := func()(string, error){ + + switch attributeName{ + + case "AutismRiskScore":{ + return "Autism Risk Score", nil + } + case "OffspringAutismRiskScore":{ + return "Offspring Autism Risk Score", nil + } + case "ObesityRiskScore":{ + return "Obesity Risk Score", nil + } + case "OffspringObesityRiskScore":{ + return "Offspring Obesity Risk Score", nil + } + case "HomosexualnessScore":{ + return "Homosexualness Score", nil + } + case "OffspringHomosexualnessScore":{ + return "Offspring Homosexualness Score", nil + } + } + + return "", errors.New("getAttributeTitle reached with unknown attributeName: " + attributeName) + } + + attributeTitle, err := getAttributeTitle() + if (err != nil) { return "", false, nil, "", "", err } + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(attributeTitle) + + return titleTranslated, true, passValueFunction, "/10", unknownTranslated, nil + } + case "PredictedEyeColor":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Predicted Eye Color") + + return titleTranslated, false, translateValueFunction, "", unknownTranslated, nil + } + case "PredictedHairTexture":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Predicted Hair Texture") + + return titleTranslated, false, translateValueFunction, "", unknownTranslated, nil + } + case "PredictedLactoseTolerance":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Predicted Lactose Tolerance") + + return titleTranslated, false, translateValueFunction, "", unknownTranslated, nil + } + case "OffspringBlueEyesProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Blue Eyes Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } + case "OffspringGreenEyesProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Green Eyes Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } + case "OffspringHazelEyesProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Hazel Eyes Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } + case "OffspringBrownEyesProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Brown Eyes Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } + case "OffspringLactoseToleranceProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Lactose Tolerance Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } + case "OffspringStraightHairProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Straight Hair Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } + case "OffspringCurlyHairProbability":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Curly Hair Probability") + + return titleTranslated, false, formatPercentageFunction, "%", unknownTranslated, nil + } } attributeHasMonogenicDiseasePrefix := strings.HasPrefix(attributeName, "MonogenicDisease_") diff --git a/internal/profiles/calculatedAttributes/calculatedAttributes.go b/internal/profiles/calculatedAttributes/calculatedAttributes.go index b589b12..02414e5 100644 --- a/internal/profiles/calculatedAttributes/calculatedAttributes.go +++ b/internal/profiles/calculatedAttributes/calculatedAttributes.go @@ -46,10 +46,6 @@ import "slices" //TODO: // -LastActive -// -OffspringLactoseToleranceProbability -// Used to sort users based on probability of lactose tolerance -// This allows the user to sort matches based on whose offspring is most likely to be lactose tolerant -// -Offspring Probability for all traits // -DietSimilarity @@ -62,12 +58,6 @@ var calculatedAttributesList = []string{ "Distance", "IsSameSex", "23andMe_OffspringNeanderthalVariants", - "OffspringProbabilityOfAnyMonogenicDisease", - "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested", - "TotalPolygenicDiseaseRiskScore", - "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", - "OffspringTotalPolygenicDiseaseRiskScore", - "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", "SearchTermsCount", "HasMessagedMe", "IHaveMessaged", @@ -95,6 +85,46 @@ var calculatedAttributesList = []string{ "23andMe_MaternalHaplogroupSimilarity", "23andMe_PaternalHaplogroupSimilarity", "NumberOfReviews", + + "OffspringProbabilityOfAnyMonogenicDisease", + "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested", + + // Polygenic Diseases: + + "TotalPolygenicDiseaseRiskScore", + "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", + + "OffspringTotalPolygenicDiseaseRiskScore", + "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", + + "AutismRiskScore", + "OffspringAutismRiskScore", + + "ObesityRiskScore", + "OffspringObesityRiskScore", + + // Discrete Traits: + + "PredictedEyeColor", + "OffspringBlueEyesProbability", + "OffspringGreenEyesProbability", + "OffspringHazelEyesProbability", + "OffspringBrownEyesProbability", + + "PredictedLactoseTolerance", + "OffspringLactoseToleranceProbability", + + "PredictedHairTexture", + "OffspringStraightHairProbability", + "OffspringCurlyHairProbability", + + // Numeric Traits: + + "HomosexualnessScore", + "OffspringHomosexualnessScore", + + "PredictedHeight", + "OffspringPredictedHeight", } // We use a map for faster lookups @@ -677,15 +707,22 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA // Each risk score is a number between 0 and 1 allDiseasesAverageRiskScoreNumerator := float64(0) - for _, diseaseObject := range polygenicDiseaseObjectsList{ + // Map Structure: Locus rsID -> Locus Value + userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) - // Map Structure: Locus rsID -> Locus Value - userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) + for _, diseaseObject := range polygenicDiseaseObjectsList{ diseaseLociList := diseaseObject.LociList for _, locusRSID := range diseaseLociList{ + _, exists := userDiseaseLocusValuesMap[locusRSID] + if (exists == true){ + // We already added this locus + // This can happen if two diseases share the same locus + continue + } + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) locusValueAttributeName := "LocusValue_rs" + locusRSIDString @@ -769,11 +806,8 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ // We have not linked a genome person and performed a genetic analysis - // The total monogenic disease risk is unknown - // We can still predict disease risk for individual recessive disorders when one person has no variants, but - // not the total probability for all monogenic diseases - // This is because everyone is a carrier for at least some recessive monogenic disorders - // + // The total polygenic disease risk is unknown + return false, profileVersion, "", nil } @@ -794,15 +828,22 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA // Each risk score is a number between 0 and 1 allDiseasesAverageRiskScoreNumerator := float64(0) + // Map Structure: rsID -> Locus Value + userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) + for _, diseaseObject := range polygenicDiseaseObjectsList{ diseaseLociList := diseaseObject.LociList - // Map Structure: rsID -> Locus Value - userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) - for _, locusRSID := range diseaseLociList{ + _, exists := userDiseaseLocusValuesMap[locusRSID] + if (exists == true){ + // We already added this locus + // This can happen if two diseases share the same locus + continue + } + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) locusValueAttributeName := "LocusValue_rs" + locusRSIDString @@ -834,7 +875,7 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA Base2Value: userLocusBase2, LocusIsPhased: userLocusIsPhased, } - + userDiseaseLocusValuesMap[locusRSID] = newLocusValueObject } @@ -867,6 +908,386 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA return true, profileVersion, allDiseasesAverageRiskScoreString, nil } + case "AutismRiskScore", + "ObesityRiskScore":{ + + // These are polygenic diseases + // We get the risk score for the user + + diseaseName := strings.TrimSuffix(attributeName, "RiskScore") + + diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName) + if (err != nil){ return false, 0, "", err } + + diseaseLociList := diseaseObject.LociList + + userDiseaseLocusValuesMap, err := getUserGenomeLocusValuesMap(diseaseLociList, getProfileAttributesFunction) + if (err != nil) { return false, 0, "", err } + + neuralNetworkExists, anyLocusTested, userDiseaseRiskScore, _, _, _, err := createPersonGeneticAnalysis.GetPersonGenomePolygenicDiseaseAnalysis(diseaseObject, userDiseaseLocusValuesMap, true) + if (err != nil) { return false, 0, "", err } + if (neuralNetworkExists == false){ + return false, 0, "", errors.New("Neural network missing for disease: " + diseaseName) + } + if (anyLocusTested == false){ + // Disease risk is unknown + return false, profileVersion, "", nil + } + + riskScoreString := helpers.ConvertIntToString(userDiseaseRiskScore) + + return true, profileVersion, riskScoreString, nil + } + case "OffspringAutismRiskScore", + "OffspringObesityRiskScore":{ + + // These are polygenic diseases + // We get the risk score for the offspring + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myGeneticAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, 0, "", err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + // We have not linked a genome person and performed a genetic analysis + // All offspring polygenic disease risks are unknown + + 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.") + } + + + diseaseNameWithOffspring := strings.TrimSuffix(attributeName, "RiskScore") + + diseaseName := strings.TrimPrefix(diseaseNameWithOffspring, "Offspring") + + diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName) + if (err != nil){ return false, 0, "", err } + + diseaseLociList := diseaseObject.LociList + + userDiseaseLocusValuesMap, err := getUserGenomeLocusValuesMap(diseaseLociList, getProfileAttributesFunction) + if (err != nil) { return false, 0, "", err } + + neuralNetworkExists, anyLocusValuesTested, offspringAverageRiskScore, _, _, _, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseAnalysis(diseaseObject, myGenomeLocusValuesMap, userDiseaseLocusValuesMap) + if (err != nil) { return false, 0, "", err } + if (neuralNetworkExists == false){ + return false, 0, "", errors.New("No neural network exists for disease: " + diseaseName) + } + if (anyLocusValuesTested == false){ + // No disease loci are known + return false, profileVersion, "", nil + } + + offspringAverageRiskScoreString := helpers.ConvertIntToString(offspringAverageRiskScore) + + return true, profileVersion, offspringAverageRiskScoreString, nil + } + case "PredictedEyeColor", + "PredictedLactoseTolerance", + "PredictedHairTexture":{ + + //Outputs: + // -string: Trait name + getTraitName := func()(string, error){ + + switch attributeName{ + + case "PredictedEyeColor":{ + return "Eye Color", nil + } + case "PredictedLactoseTolerance":{ + return "Lactose Tolerance", nil + } + case "PredictedHairTexture":{ + return "Hair Texture", nil + } + } + + return "", errors.New("Discrete trait calculated attribute reached with unknown attributeName: " + attributeName) + } + + traitName, err := getTraitName() + if (err != nil) { return false, 0, "", err } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, 0, "", err } + + traitLociList := traitObject.LociList + + userTraitLocusValuesMap, err := getUserGenomeLocusValuesMap(traitLociList, getProfileAttributesFunction) + if (err != nil) { return false, 0, "", err } + + neuralNetworkExists, neuralNetworkOutcomeIsKnown, predictedOutcome, _, _, _, err := createPersonGeneticAnalysis.GetGenomeDiscreteTraitAnalysis_NeuralNetwork(traitObject, userTraitLocusValuesMap, true) + if (err != nil) { return false, 0, "", err } + if (neuralNetworkExists == true){ + if (neuralNetworkOutcomeIsKnown == false){ + return false, 0, "", nil + } + + return true, profileVersion, predictedOutcome, nil + } + + anyRulesExist, _, _, _, predictedOutcomeExists, predictedOutcome, err := createPersonGeneticAnalysis.GetGenomeDiscreteTraitAnalysis_Rules(traitObject, userTraitLocusValuesMap, true) + if (err != nil) { return false, 0, "", err } + if (anyRulesExist == false){ + return false, 0, "", errors.New("Discrete trait calculated attribute exists for trait without a neural network or rules: " + traitName) + } + if (predictedOutcomeExists == false){ + return false, 0, "", nil + } + + return true, profileVersion, predictedOutcome, nil + } + case "OffspringBlueEyesProbability", + "OffspringGreenEyesProbability", + "OffspringHazelEyesProbability", + "OffspringBrownEyesProbability", + "OffspringLactoseToleranceProbability", + "OffspringStraightHairProbability", + "OffspringCurlyHairProbability":{ + + //TODO: Add ability to retrieve confidence/quantity of loci known and sort/filter by those attributes + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myGeneticAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, 0, "", err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + // We have not linked a genome person and performed a genetic analysis + // All offspring trait predictions are unknown + + 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.") + } + + // These are discrete traits + // We get the outcome probability for the offspring + + //Outputs: + // -string: Trait name + // -string: Outcome name + getTraitAndOutcomeName := func()(string, string, error){ + + switch attributeName{ + + case "OffspringBlueEyesProbability":{ + return "Eye Color", "Blue", nil + } + case "OffspringGreenEyesProbability":{ + return "Eye Color", "Green", nil + } + case "OffspringHazelEyesProbability":{ + return "Eye Color", "Hazel", nil + } + case "OffspringBrownEyesProbability":{ + return "Eye Color", "Brown", nil + } + case "OffspringLactoseToleranceProbability":{ + return "Lactose Tolerance", "Tolerant", nil + } + case "OffspringStraightHairProbability":{ + return "Hair Texture", "Straight", nil + } + case "OffspringCurlyHairProbability":{ + return "Hair Texture", "Curly", nil + } + } + + return "", "", errors.New("Offspring discrete trait calculated attribute reached with unknown attributeName: " + attributeName) + } + + traitName, outcomeName, err := getTraitAndOutcomeName() + if (err != nil) { return false, 0, "", err } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, 0, "", err } + + traitLociList := traitObject.LociList + + userTraitLocusValuesMap, err := getUserGenomeLocusValuesMap(traitLociList, getProfileAttributesFunction) + if (err != nil) { return false, 0, "", err } + + neuralNetworkExists, anyLociKnown, outcomeProbabilitiesMap, _, _, _, err := createCoupleGeneticAnalysis.GetOffspringDiscreteTraitAnalysis_NeuralNetwork(traitObject, myGenomeLocusValuesMap, userTraitLocusValuesMap) + if (err != nil) { return false, 0, "", err } + if (neuralNetworkExists == true){ + if (anyLociKnown == false){ + + // Trait prediction is not possible + return false, 0, "", nil + } + + outcomeProbability, exists := outcomeProbabilitiesMap[outcomeName] + if (exists == false){ + return true, profileVersion, "0", nil + } + + outcomeProbabilityString := helpers.ConvertIntToString(outcomeProbability) + + return true, profileVersion, outcomeProbabilityString, nil + } + + anyRulesExist, rulesAnalysisExists, _, _, _, outcomeProbabilitiesMap, err := createCoupleGeneticAnalysis.GetOffspringDiscreteTraitAnalysis_Rules(traitObject, myGenomeLocusValuesMap, userTraitLocusValuesMap) + if (err != nil) { return false, 0, "", err } + if (anyRulesExist == false){ + return false, 0, "", errors.New("Calculation of offspring attribute for discrete trait called with trait missing neural network or rules: " + traitName) + } + if (rulesAnalysisExists == false){ + // Analysis is impossible for this trait + return false, 0, "", nil + } + + outcomeProbability, exists := outcomeProbabilitiesMap[outcomeName] + if (exists == false){ + return true, profileVersion, "0", nil + } + + outcomeProbabilityString := helpers.ConvertIntToString(outcomeProbability) + + return true, profileVersion, outcomeProbabilityString, nil + } + case "HomosexualnessScore", + "PredictedHeight":{ + + // These are numeric traits + // We calculate the value for the user + + //Outputs: + // -string: Trait name + // -bool: Is a score between 0-10 + // -error + getTraitNameAndIsAScoreBool := func()(string, bool, error){ + + switch attributeName{ + case "HomosexualnessScore":{ + return "Homosexualness", true, nil + } + case "PredictedHeight":{ + return "Height", false, nil + } + } + + return "", false, errors.New("User numeric trait value calculation called with unknown attributeName: " + attributeName) + } + + traitName, traitIsAScore, err := getTraitNameAndIsAScoreBool() + if (err != nil) { return false, 0, "", err } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, 0, "", err } + + traitLociList := traitObject.LociList + + userTraitLocusValuesMap, err := getUserGenomeLocusValuesMap(traitLociList, getProfileAttributesFunction) + if (err != nil) { return false, 0, "", err } + + traitNeuralNetworkExists, anyLocusValuesAreKnown, predictedOutcome, _, _, _, err := createPersonGeneticAnalysis.GetGenomeNumericTraitAnalysis(traitObject, userTraitLocusValuesMap, true) + if (err != nil) { return false, 0, "", err } + if (traitNeuralNetworkExists == false){ + return false, 0, "", errors.New("Numeric trait attribute calculation reached for trait with no neural network: " + traitName) + } + if (anyLocusValuesAreKnown == false){ + // Trait prediction is impossible + return false, 0, "", nil + } + + if (traitIsAScore == true){ + + predictedScoreString := helpers.ConvertIntToString(int(predictedOutcome)) + + return true, profileVersion, predictedScoreString, nil + } + + predictedOutcomeString := helpers.ConvertFloat64ToString(predictedOutcome) + + return true, profileVersion, predictedOutcomeString, nil + } + case "OffspringHomosexualnessScore", + "OffspringPredictedHeight":{ + + // These are numeric traits + // We calculate the value for the offspring + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myGeneticAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, 0, "", err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + // We have not linked a genome person and performed a genetic analysis + // All offspring trait predictions are unknown + + 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.") + } + + //Outputs: + // -string: Trait name + // -bool: Is a score between 0-10 + // -error + getTraitNameAndIsAScoreBool := func()(string, bool, error){ + + switch attributeName{ + case "OffspringHomosexualnessScore":{ + return "Homosexualness", true, nil + } + case "OffspringPredictedHeight":{ + return "Height", false, nil + } + } + + return "", false, errors.New("Offspring numeric trait value calculation called with unknown attributeName: " + attributeName) + } + + traitName, traitIsAScore, err := getTraitNameAndIsAScoreBool() + if (err != nil) { return false, 0, "", err } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, 0, "", err } + + traitLociList := traitObject.LociList + + userTraitLocusValuesMap, err := getUserGenomeLocusValuesMap(traitLociList, getProfileAttributesFunction) + if (err != nil) { return false, 0, "", err } + + neuralNetworkExists, anyLociKnown, predictedOutcome, _, _, _, _, err := createCoupleGeneticAnalysis.GetOffspringNumericTraitAnalysis(traitObject, userTraitLocusValuesMap, myGenomeLocusValuesMap) + if (err != nil) { return false, 0, "", err } + if (neuralNetworkExists == false){ + return false, 0, "", errors.New("Offspring Numeric trait attribute calculation reached for trait with no neural network: " + traitName) + } + if (anyLociKnown == false){ + // Prediction is impossible + return false, profileVersion, "", nil + } + + if (traitIsAScore == true){ + + predictedScoreString := helpers.ConvertIntToString(int(predictedOutcome)) + + return true, profileVersion, predictedScoreString, nil + } + + predictedOutcomeString := helpers.ConvertFloat64ToString(predictedOutcome) + + return true, profileVersion, predictedOutcomeString, nil + } case "SearchTermsCount":{ myDesireExists, myDesiredChoicesListString, err := myLocalDesires.GetDesire("SearchTerms") @@ -1645,7 +2066,55 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with unknown attribute: " + attributeName) } - +// This function constructs a traitLocusValuesMap from a user's profile and a set of loci +//Outputs: +// -map[int64]locusValue.LocusValue: Genome map for provided loci +// -error +func getUserGenomeLocusValuesMap(lociList []int64, getProfileAttributesFunction func(string)(bool, int, string, error))(map[int64]locusValue.LocusValue, error){ + + // We construct the user's locus values map + // Map Structure: Locus rsID -> locusValue.LocusValue + userTraitLocusValuesMap := make(map[int64]locusValue.LocusValue) + + for _, rsID := range lociList{ + + rsIDString := helpers.ConvertInt64ToString(rsID) + + userLocusValueAttributeName := "LocusValue_rs" + rsIDString + + userLocusValueIsKnown, _, userLocusValue, err := getProfileAttributesFunction(userLocusValueAttributeName) + if (err != nil) { return nil, err } + if (userLocusValueIsKnown == false){ + continue + } + + userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusValue, ";") + if (semicolonFound == false){ + return nil, errors.New("Database corrupt: Contains profile with invalid " + userLocusValueAttributeName + " value: " + userLocusValue) + } + + userLocusIsPhasedAttributeName := "LocusIsPhased_rs" + rsIDString + + userLocusIsPhasedExists, _, userLocusIsPhasedString, err := getProfileAttributesFunction(userLocusIsPhasedAttributeName) + if (err != nil) { return nil, err } + if (userLocusIsPhasedExists == false){ + return nil, errors.New("Database corrupt: Contains profile with locusValue but not locusIsPhased status for locus: " + rsIDString) + } + + userLocusIsPhased, err := helpers.ConvertYesOrNoStringToBool(userLocusIsPhasedString) + if (err != nil) { return nil, err } + + userLocusValueObject := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + LocusIsPhased: userLocusIsPhased, + } + + userTraitLocusValuesMap[rsID] = userLocusValueObject + } + + return userTraitLocusValuesMap, nil +} diff --git a/internal/profiles/calculatedAttributes/calculatedAttributes_test.go b/internal/profiles/calculatedAttributes/calculatedAttributes_test.go index 0d54196..13c60f4 100644 --- a/internal/profiles/calculatedAttributes/calculatedAttributes_test.go +++ b/internal/profiles/calculatedAttributes/calculatedAttributes_test.go @@ -26,7 +26,7 @@ func TestCalculatedAttributes(t *testing.T){ calculatedAttributesList := calculatedAttributes.GetCalculatedAttributesList() - for i:=0; i<100; i++{ + for i:=0; i<10; i++{ identityPublicKey, identityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() if (err != nil){