// calculatedAttributes provides functions to retrieve calculated profile attributes // These are attributes that are constructed, rather than being retrieved from a profileMap directly. // They can be constructed using profile attributes, a user's desires, and other information. // An example is Distance, which requires calculating the distance between another user's coordinates and our own package calculatedAttributes import "seekia/imported/geodist" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" import "seekia/internal/badgerDatabase" import "seekia/internal/convertCurrencies" import "seekia/internal/desires/myLocalDesires" import "seekia/internal/desires/myMateDesires" import "seekia/internal/encoding" import "seekia/internal/genetics/companyAnalysis" import "seekia/internal/genetics/createCoupleGeneticAnalysis" import "seekia/internal/genetics/createPersonGeneticAnalysis" import "seekia/internal/genetics/locusValue" import "seekia/internal/genetics/myChosenAnalysis" import "seekia/internal/genetics/readGeneticAnalysis" import "seekia/internal/helpers" import "seekia/internal/identity" import "seekia/internal/messaging/myChatMessages" import "seekia/internal/moderation/moderatorControversy" import "seekia/internal/moderation/moderatorScores" import "seekia/internal/moderation/reviewStorage" import "seekia/internal/myContacts" import "seekia/internal/myIdentity" import "seekia/internal/myIgnoredUsers" import "seekia/internal/myLikedUsers" import "seekia/internal/myMatchScore" import "seekia/internal/profiles/myLocalProfiles" import "seekia/internal/profiles/readProfiles" import messagepack "github.com/vmihailenco/msgpack/v5" import "sync" import "strings" import "errors" 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 var calculatedAttributesList = []string{ "IdentityHash", "IdentityScore", "MatchScore", "Controversy", "BanAdvocates", "Distance", "IsSameSex", "23andMe_OffspringNeanderthalVariants", "OffspringProbabilityOfAnyMonogenicDisease", "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested", "TotalPolygenicDiseaseRiskScore", "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", "OffspringTotalPolygenicDiseaseRiskScore", "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", "SearchTermsCount", "HasMessagedMe", "IHaveMessaged", "HasRejectedMe", "IsLiked", "IsIgnored", "IsMyContact", "WealthInGold", "RacialSimilarity", "EyeColorSimilarity", "EyeColorGenesSimilarity", "EyeColorGenesSimilarity_NumberOfSimilarAlleles", "HairColorSimilarity", "HairColorGenesSimilarity", "HairColorGenesSimilarity_NumberOfSimilarAlleles", "SkinColorSimilarity", "SkinColorGenesSimilarity", "SkinColorGenesSimilarity_NumberOfSimilarAlleles", "HairTextureSimilarity", "HairTextureGenesSimilarity", "HairTextureGenesSimilarity_NumberOfSimilarAlleles", "FacialStructureGenesSimilarity", "FacialStructureGenesSimilarity_NumberOfSimilarAlleles", "23andMe_AncestralSimilarity", "23andMe_MaternalHaplogroupSimilarity", "23andMe_PaternalHaplogroupSimilarity", "NumberOfReviews", } // We use a map for faster lookups var calculatedAttributesMap map[string]struct{} // We use this function to initialize the calculatedAttributesMap func init(){ calculatedAttributesMap = make(map[string]struct{}) for _, attributeName := range calculatedAttributesList{ calculatedAttributesMap[attributeName] = struct{}{} } } // We only use this function for testing func GetCalculatedAttributesList()[]string{ return calculatedAttributesList } func GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion int, rawProfileMap map[int]messagepack.RawMessage)(func(string)(bool, int, string, error), error){ // We use this map to store formatted profile values // We do this so we only have to format values once for all uses of the getAnyAttributeFunction iteration formattedProfileMap := make(map[string]string) // We use this mutex so it is safe to call the getAnyAttributeFunction concurrently var formattedProfileMapMutex sync.RWMutex getAttributeFunction := func(attributeName string)(bool, int, string, error){ formattedProfileMapMutex.RLock() formattedAttributeValue, exists := formattedProfileMap[attributeName] formattedProfileMapMutex.RUnlock() if (exists == true){ return true, profileVersion, formattedAttributeValue, nil } // Now we check the rawProfileMap attributeExists, formattedAttributeValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, attributeName) if (err != nil) { return false, 0, "", err } if (attributeExists == false){ return false, profileVersion, "", nil } // We write the formatted value to the formattedProfileMap and return it formattedProfileMapMutex.Lock() formattedProfileMap[attributeName] = formattedAttributeValue formattedProfileMapMutex.Unlock() return true, profileVersion, formattedAttributeValue, nil } getAnyAttributeFunction := func(attributeName string)(bool, int, string, error){ attributeExists, _, attributeValue, err := GetAnyProfileAttributeIncludingCalculated(attributeName, getAttributeFunction) if (err != nil) { return false, 0, "", err } if (attributeExists == false){ return false, profileVersion, "", nil } return true, profileVersion, attributeValue, nil } return getAnyAttributeFunction, nil } //Outputs: // -bool: Attribute value is known // -int: Profile version that attribute was derived from. // This does not matter for calculated attributes, which will always return the same kind of result, regardless of profile versions. // -string: Attribute value // -error func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileAttributesFunction func(string)(bool, int, string, error))(bool, int, string, error){ _, isCalulatedAttribute := calculatedAttributesMap[attributeName] if (isCalulatedAttribute == false){ exists, profileVersion, retrievedAttribute, err := getProfileAttributesFunction(attributeName) if (err != nil) { return false, 0, "", err } if (exists == false) { return false, 0, "", nil } return true, profileVersion, retrievedAttribute, nil } exists, profileVersion, profileType, err := getProfileAttributesFunction("ProfileType") if (err != nil) { return false, 0, "", err } if (exists == false) { return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with profile missing ProfileType") } getUserIdentityHash := func()([16]byte, error){ exists, _, identityKeyHex, err := getProfileAttributesFunction("IdentityKey") if (err != nil) { return [16]byte{}, err } if (exists == false) { return [16]byte{}, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile missing IdentityKey") } identityKeyBytes, err := encoding.DecodeHexStringToBytes(identityKeyHex) if (err != nil){ return [16]byte{}, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing non-hex IdentityKey: " + identityKeyHex) } if (len(identityKeyBytes) != 32){ return [16]byte{}, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid IdentityKey: Invalid length: " + identityKeyHex) } identityKeyArray := [32]byte(identityKeyBytes) identityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityKeyArray, profileType) if (err != nil) { return [16]byte{}, err } return identityHash, nil } getUserProfileNetworkType := func()(byte, error){ exists, _, networkTypeString, err := getProfileAttributesFunction("NetworkType") if (err != nil) { return 0, err } if (exists == false) { return 0, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile missing NetworkType") } networkType, err := helpers.ConvertNetworkTypeStringToByte(networkTypeString) if (err != nil) { return 0, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid NetworkType: " + networkTypeString) } return networkType, nil } switch attributeName{ case "IdentityHash":{ identityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) if (err != nil){ return false, 0, "", err } return true, profileVersion, identityHashString, nil } case "IdentityScore":{ if (profileType != "Moderator"){ return false, 0, "", errors.New("Trying to get identity score for non-moderator identity.") } identityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } scoreIsKnown, moderatorScore, _, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash) if (err != nil) { return false, 0, "", err } if (scoreIsKnown == false){ return false, 0, "", nil } moderatorScoreString := helpers.ConvertFloat64ToString(moderatorScore) return true, profileVersion, moderatorScoreString, nil } case "MatchScore":{ if (profileType != "Mate"){ return false, 0, "", errors.New("Trying to get match score for non mate identity.") } // Calculating match score requires retrieving other calculated attributes // We have to recursively call this function getAnyAttributeForMatchScore := func(inputAttributeName string)(bool, int, string, error){ // We make sure infinite recursion will not happen accidentally if (inputAttributeName == "MatchScore"){ return false, 0, "", errors.New("Infinite recursion occurs during match score attribute retrieval.") } exists, profileVersion, attributeValue, err := GetAnyProfileAttributeIncludingCalculated(inputAttributeName, getProfileAttributesFunction) return exists, profileVersion, attributeValue, err } allMyDesiresList := myMateDesires.GetAllMyDesiresList(false) matchScore := 0 for _, desireName := range allMyDesiresList{ myDesireExists, statusIsKnown, fulfillsDesire, err := myMateDesires.CheckIfMateProfileFulfillsMyDesire(desireName, getAnyAttributeForMatchScore) if (err != nil){ return false, 0, "", err } if (myDesireExists == false || statusIsKnown == false || fulfillsDesire == false){ continue } pointsToAdd, err := myMatchScore.GetMyMatchScoreDesirePoints(desireName) if (err != nil){ return false, 0, "", err } matchScore += pointsToAdd } matchScoreString := helpers.ConvertIntToString(matchScore) return true, profileVersion, matchScoreString, nil } case "Controversy":{ if (profileType != "Moderator"){ return false, 0, "", errors.New("Trying to get Controversy for non-Moderator identity.") } identityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } profileNetworkType, err := getUserProfileNetworkType() if (err != nil) { return false, 0, "", err } isKnown, controversyRating, err := moderatorControversy.GetModeratorControversyRating(identityHash, profileNetworkType) if (err != nil) { return false, 0, "", err } if (isKnown == false){ return false, 0, "", nil } ratingString := helpers.ConvertInt64ToString(controversyRating) return true, profileVersion, ratingString, nil } case "BanAdvocates":{ // This is the number of moderators who have banned this identity // We will return both eligible and banned ban advocates identityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } profileNetworkType, err := getUserProfileNetworkType() if (err != nil) { return false, 0, "", err } downloadingRequiredReviews, numberOfBanAdvocates, err := reviewStorage.GetNumberOfBanAdvocatesForIdentity(identityHash, profileNetworkType) if (err != nil) { return false, 0, "", err } if (downloadingRequiredReviews == false){ return false, 0, "", nil } numberOfBanAdvocatesString := helpers.ConvertIntToString(numberOfBanAdvocates) return true, profileVersion, numberOfBanAdvocatesString, nil } case "Distance":{ if (profileType != "Mate"){ return false, 0, "", errors.New("Trying to get Distance for non-Mate identity.") } exists, _, theirLocationLatitudeString, err := getProfileAttributesFunction("PrimaryLocationLatitude") if (err != nil) { return false, 0, "", err } if (exists == false){ return false, 0, "", nil } exists, _, theirLocationLongitudeString, err := getProfileAttributesFunction("PrimaryLocationLongitude") if (err != nil) { return false, 0, "", err } if (exists == false){ return false, 0, "", errors.New("Profile malformed when trying to calculate distance: contains PrimaryLocationLatitude and not PrimaryLocationLongitude") } theirLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(theirLocationLatitudeString) if (err != nil) { return false, 0, "", errors.New("Profile malformed when trying to calculate distance: contains invalid PrimaryLocationLatitude: " + theirLocationLatitudeString) } theirLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(theirLocationLongitudeString) if (err != nil) { return false, 0, "", errors.New("Profile malformed when trying to calculate distance: contains invalid PrimaryLocationLongitude: " + theirLocationLongitudeString) } exists, myLocationLatitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLatitude") if (err != nil) { return false, 0, "", err } if (exists == false){ return false, 0, "", nil } exists, myLocationLongitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLongitude") if (err != nil) { return false, 0, "", err } if (exists == false){ return false, 0, "", errors.New("MyLocalProfiles contains PrimaryLocationLatitude but not PrimaryLocationLongitude") } myLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLatitudeString) if (err != nil) { return false, 0, "", errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString) } myLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLongitudeString) if (err != nil) { return false, 0, "", errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString) } distanceInKilometers, err := geodist.GetDistanceBetweenCoordinates(myLocationLatitudeFloat64, myLocationLongitudeFloat64, theirLocationLatitudeFloat64, theirLocationLongitudeFloat64) if (err != nil){ return false, 0, "", err } distanceString := helpers.ConvertFloat64ToString(distanceInKilometers) return true, profileVersion, distanceString, nil } case "IsSameSex":{ // We use this to see if the user is the same sex as us // We then know if we should display offspring attributes // If they are the same sex, reproduction is impossible, so showing the offspring data is pointless // We return Unknown if either person is of one of the intersex sexes // This way, offspring data will be shown unless a Male is viewing a Female, or vice versa. mySexExists, mySex, err := myLocalProfiles.GetProfileData("Mate", "Sex") if (err != nil) { return false, 0, "", err } if (mySexExists == false){ return false, 0, "", nil } if (mySex != "Male" && mySex != "Female"){ return false, 0, "", nil } userSexExists, _, userSex, err := getProfileAttributesFunction("Sex") if (err != nil) { return false, 0, "", err } if (userSexExists == false){ return false, 0, "", nil } if (userSex != "Male" && userSex != "Female"){ return false, 0, "", nil } if (mySex == userSex){ return true, profileVersion, "Yes", nil } return true, profileVersion, "No", nil } case "23andMe_OffspringNeanderthalVariants":{ myNeanderthalVariantsExist, myNeanderthalVariants, err := myLocalProfiles.GetProfileData("Mate", "23andMe_NeanderthalVariants") if (err != nil) { return false, 0, "", err } if (myNeanderthalVariantsExist == false){ return false, 0, "", nil } myNeanderthalVariantsInt, err := helpers.ConvertStringToInt(myNeanderthalVariants) if (err != nil) { return false, 0, "", errors.New("MyLocalProfiles malformed: Contains invalid 23andMe_NeanderthalVariants: " + myNeanderthalVariants) } userNeanderthalVariantsExist, _, userNeanderthalVariants, err := getProfileAttributesFunction("23andMe_NeanderthalVariants") if (err != nil) { return false, 0, "", err } if (userNeanderthalVariantsExist == false){ return false, 0, "", nil } userNeanderthalVariantsInt, err := helpers.ConvertStringToInt(userNeanderthalVariants) if (err != nil){ return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid 23andMe_NeanderthalVariants: " + userNeanderthalVariants) } // We take the average of both offspringNeanderthalVariants := (myNeanderthalVariantsInt + userNeanderthalVariantsInt)/2 offspringNeanderthalVariantsString := helpers.ConvertIntToString(offspringNeanderthalVariants) return true, profileVersion, offspringNeanderthalVariantsString, nil } case "OffspringProbabilityOfAnyMonogenicDisease", "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested":{ // We say the probability is known if: // 1. At least 1 tested variant exists for both people for the same monogenic disease // 2. For any monogenic diseases where either person has a non-zero probability, we know the offspring has a 0 probability // For users using the 0% desire, they can be sure that: // 1. Their matches have at least been analyzed by 1 company. // 2. Their matches will never be carriers for the same monogenic disease(s) as them. // TODO Users should eventually be able to filter users based on how many variants the offspring has been tested for // For example, If a user has had their entire genome sequenced, they will be able to only show other users who have done the same 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 // 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 // return false, profileVersion, "", nil } monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() if (err != nil) { return false, 0, "", err } // This will be true if either we or the user have a non-zero probability of passing a variant, // but the opposite person has an unknown probability of passing a variant // This is a much higher risk scenario // For monogenic diseases where both user probabilities are unknown, this does not apply // Basically, we want to exercise a higher level of caution if we are aware of a non-zero potential risk // This will also be true if either user has any dominant monogenic diseases // Thus, if our final disease probability is 0%, and this bool is true, we will say probability is Unknown nonZeroUnknownDiseaseRiskExists := false // This will store the number of diseases we can test for // This is displayed to the user in the viewProfileGui numberOfDiseasesTested := 0 // We use this bool to track if the user has provided any monogenic disease probabilities // If they have not, then we will return "Unknown" for the same reason we described for when our own analysis does not exist //TODO: Require both probabilities to exist for the same disease at least once? anyUserProbabilityIsKnown := false // This stores the probability of the offspring having each tested disease // Each float in this list is a value between 0-1 allDiseaseProbabilitiesList := make([]float64, 0) for _, diseaseObject := range monogenicDiseaseObjectsList{ monogenicDiseaseName := diseaseObject.DiseaseName diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") probabilityOfPassingAVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" userProbabilityIsKnown, _, userProbabilityOfPassingADiseaseVariant, err := getProfileAttributesFunction(probabilityOfPassingAVariantAttributeName) if (err != nil) { return false, 0, "", err } if (userProbabilityIsKnown == true){ anyUserProbabilityIsKnown = true } myProbabilityIsKnown, _, myProbabilityOfPassingADiseaseVariant, _, _, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, monogenicDiseaseName, myGenomeIdentifier) if (err != nil) { return false, 0, "", err } if (userProbabilityIsKnown == false && myProbabilityIsKnown == false){ continue } getUserProbabilityOfPassingADiseaseVariantInt := func()(int, error){ if (userProbabilityIsKnown == false){ return 0, nil } userProbabilityOfPassingADiseaseVariantInt, err := helpers.ConvertStringToInt(userProbabilityOfPassingADiseaseVariant) if (err != nil){ return 0, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid " + probabilityOfPassingAVariantAttributeName + ": " + userProbabilityOfPassingADiseaseVariant) } return userProbabilityOfPassingADiseaseVariantInt, nil } userProbabilityOfPassingADiseaseVariantInt, err := getUserProbabilityOfPassingADiseaseVariantInt() if (err != nil) { return false, 0, "", err } probabilityOffspringHasDiseaseIsKnown, offspringPercentageProbabilityOfDisease, _, _, err := createCoupleGeneticAnalysis.GetOffspringMonogenicDiseaseProbabilities(diseaseIsDominantOrRecessive, myProbabilityIsKnown, myProbabilityOfPassingADiseaseVariant, userProbabilityIsKnown, userProbabilityOfPassingADiseaseVariantInt) if (err != nil) { return false, 0, "", err } if (probabilityOffspringHasDiseaseIsKnown == false){ // We do not know the probability the offspring will have this disease // We check to see if either person has a non-zero risk // If so, the probability of the offspring having the disease is potentially >0 getNonZeroUnknownDiseaseRiskExistsBool := func()bool{ if (diseaseIsDominantOrRecessive == "Recessive"){ // We know there exists a non-zero risk // We know that at least 1 of the two people has a known pass-the-variant probability // If either person had a 0% probability of passing a variant, the // GetOffspringMonogenicDiseaseProbabilities function would have returned a 0% risk of /// the offspring having the disease. // Thus, there exists the possibility of the offspring having the disease // We will warn the user about this, so they can get tested and make sure they are not // a carrier for the same disease return true } // diseaseIsDominantOrRecessive == "Dominant" if (userProbabilityIsKnown == true && userProbabilityOfPassingADiseaseVariantInt != 0){ return true } if (myProbabilityIsKnown == true && myProbabilityOfPassingADiseaseVariant != 0){ return true } return false } nonZeroUnknownDiseaseRiskExists = getNonZeroUnknownDiseaseRiskExistsBool() continue } numberOfDiseasesTested += 1 if (attributeName == "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested"){ // We don't care about retrieving the total probability, so we can skip to the next disease continue } if (offspringPercentageProbabilityOfDisease == 100){ // Probability that offspring will have a disease is 100% return true, profileVersion, "100", nil } offspringProbabilityOfDisease := float64(offspringPercentageProbabilityOfDisease)/float64(100) allDiseaseProbabilitiesList = append(allDiseaseProbabilitiesList, offspringProbabilityOfDisease) } if (anyUserProbabilityIsKnown == false){ // We need at least 1 disease probability from the user for this attribute's status to be known return false, profileVersion, "", nil } if (attributeName == "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested"){ numberOfDiseasesTestedString := helpers.ConvertIntToString(numberOfDiseasesTested) return true, profileVersion, numberOfDiseasesTestedString, nil } if (len(allDiseaseProbabilitiesList) == 0){ return false, profileVersion, "", nil } // We need to find the probability of any of the probabilities occurring // Inclusive OR for all probabilities in the list // P(At least one disease) = 1 − P(No disease) probabilityOfNoMonogenicDiseases := float64(1) for _, diseaseProbability := range allDiseaseProbabilitiesList{ if (diseaseProbability < 0 || diseaseProbability > 1){ return false, 0, "", errors.New("allDiseaseProbabilitiesList contains invalid disease probability.") } // We multiply by the probability of no disease probabilityOfNoMonogenicDiseases *= (1 - diseaseProbability) } probabilityOfAtLeast1Disease := 1 - probabilityOfNoMonogenicDiseases if (probabilityOfAtLeast1Disease == 0 && nonZeroUnknownDiseaseRiskExists == true){ return false, profileVersion, "", nil } totalRiskProbabilityPercentage := probabilityOfAtLeast1Disease * 100 totalRiskProbabilityPercentageString := helpers.ConvertFloat64ToStringRounded(totalRiskProbabilityPercentage, 0) return true, profileVersion, totalRiskProbabilityPercentageString, nil } case "TotalPolygenicDiseaseRiskScore", "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested":{ // This attribute describes the user's total polygenic disease risk score // This enables users to choose a mate who has a low risk of polygenic diseases // The value is a number between 0 and 100 //TODO: Users should be able to filter by the number of loci and diseases tested polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() if (err != nil) { return false, 0, "", err } numberOfDiseasesTested := 0 // This variable adds up the risk score for each disease // 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) diseaseLociList := diseaseObject.LociList for _, locusRSID := range diseaseLociList{ locusRSIDString := helpers.ConvertInt64ToString(locusRSID) locusValueAttributeName := "LocusValue_rs" + locusRSIDString userLocusValueExists, _, userLocusValue, err := getProfileAttributesFunction(locusValueAttributeName) if (err != nil) { return false, 0, "", err } if (userLocusValueExists == false){ continue } userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusValue, ";") if (semicolonFound == false){ return false, 0, "", errors.New("Database corrupt: Contains profile with invalid " + locusValueAttributeName + " value: " + userLocusValue) } userLocusIsPhasedAttributeName := "LocusIsPhased_rs" + locusRSIDString userLocusIsPhasedExists, _, userLocusIsPhasedString, err := getProfileAttributesFunction(userLocusIsPhasedAttributeName) if (err != nil) { return false, 0, "", err } if (userLocusIsPhasedExists == false){ return false, 0, "", errors.New("Database corrupt: Contains profile with locusValue but not locusIsPhased status for locus: " + locusRSIDString) } userLocusIsPhased, err := helpers.ConvertYesOrNoStringToBool(userLocusIsPhasedString) if (err != nil) { return false, 0, "", err } userLocusValueObject := locusValue.LocusValue{ Base1Value: userLocusBase1, Base2Value: userLocusBase2, LocusIsPhased: userLocusIsPhased, } userDiseaseLocusValuesMap[locusRSID] = userLocusValueObject } neuralNetworkExists, anyLocusTested, userDiseaseRiskScore, _, _, _, err := createPersonGeneticAnalysis.GetPersonGenomePolygenicDiseaseAnalysis(diseaseObject, userDiseaseLocusValuesMap, true) if (err != nil) { return false, 0, "", err } if (neuralNetworkExists == false || anyLocusTested == false){ continue } numberOfDiseasesTested += 1 userRiskScoreFraction := float64(userDiseaseRiskScore)/float64(10) allDiseasesAverageRiskScoreNumerator += userRiskScoreFraction } if (attributeName == "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested"){ numberOfDiseasesTestedString := helpers.ConvertIntToString(numberOfDiseasesTested) return true, profileVersion, numberOfDiseasesTestedString, nil } if (numberOfDiseasesTested == 0){ return false, profileVersion, "", nil } allDiseasesAverageRiskScore := (float64(allDiseasesAverageRiskScoreNumerator)/float64(numberOfDiseasesTested)) * 100 allDiseasesAverageRiskScoreString := helpers.ConvertFloat64ToStringRounded(allDiseasesAverageRiskScore, 0) return true, profileVersion, allDiseasesAverageRiskScoreString, nil } case "OffspringTotalPolygenicDiseaseRiskScore", "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested":{ // This attribute is used to show the offspring's total risk score for all polygenic diseases // This enables users to choose a mate whose offspring will have the lowest polygenic disease risk // The value is a number between 0 and 100 // TODO Users should eventually be able to filter users based on how many loci/diseases the offspring has been tested for // For example, if a user has had their entire genome sequenced, they will be able to only show other users who have done the same //TODO: We should also be weighting the diseases based on how bad they are. // For example, Breast Cancer is not as bad as Epilepsy // LifeView.com weights each disease in their polygenic disease risk score calculation 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 // 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 // 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 } numberOfDiseasesTested := 0 // This variable adds up the risk score for each disease // Each risk score is a number between 0 and 1 allDiseasesAverageRiskScoreNumerator := float64(0) for _, diseaseObject := range polygenicDiseaseObjectsList{ diseaseLociList := diseaseObject.LociList // Map Structure: rsID -> Locus Value userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) for _, locusRSID := range diseaseLociList{ locusRSIDString := helpers.ConvertInt64ToString(locusRSID) locusValueAttributeName := "LocusValue_rs" + locusRSIDString userLocusValueExists, _, userLocusValue, err := getProfileAttributesFunction(locusValueAttributeName) if (err != nil) { return false, 0, "", err } if (userLocusValueExists == false){ continue } userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusValue, ";") if (semicolonFound == false){ return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid " + locusValueAttributeName + ": " + userLocusValue) } userLocusIsPhasedAttributeName := "LocusIsPhased_rs" + locusRSIDString userLocusIsPhasedExists, _, userLocusIsPhasedString, err := getProfileAttributesFunction(userLocusIsPhasedAttributeName) if (err != nil) { return false, 0, "", err } if (userLocusIsPhasedExists == false){ return false, 0, "", errors.New("Database corrupt: Contains profile with locusValue but not locusIsPhased status for locus: " + locusRSIDString) } userLocusIsPhased, err := helpers.ConvertYesOrNoStringToBool(userLocusIsPhasedString) if (err != nil) { return false, 0, "", err } newLocusValueObject := locusValue.LocusValue{ Base1Value: userLocusBase1, Base2Value: userLocusBase2, LocusIsPhased: userLocusIsPhased, } userDiseaseLocusValuesMap[locusRSID] = newLocusValueObject } neuralNetworkExists, anyLocusValuesTested, offspringAverageRiskScore, _, _, _, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseAnalysis(diseaseObject, myGenomeLocusValuesMap, userDiseaseLocusValuesMap) if (err != nil) { return false, 0, "", err } if (neuralNetworkExists == false || anyLocusValuesTested == false){ continue } numberOfDiseasesTested += 1 offspringRiskScoreFraction := float64(offspringAverageRiskScore)/float64(10) allDiseasesAverageRiskScoreNumerator += offspringRiskScoreFraction } if (attributeName == "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested"){ numberOfDiseasesTestedString := helpers.ConvertIntToString(numberOfDiseasesTested) return true, profileVersion, numberOfDiseasesTestedString, nil } if (numberOfDiseasesTested == 0){ return false, profileVersion, "", nil } allDiseasesAverageRiskScore := (float64(allDiseasesAverageRiskScoreNumerator)/float64(numberOfDiseasesTested)) * 100 allDiseasesAverageRiskScoreString := helpers.ConvertFloat64ToStringRounded(allDiseasesAverageRiskScore, 0) return true, profileVersion, allDiseasesAverageRiskScoreString, nil } case "SearchTermsCount":{ myDesireExists, myDesiredChoicesListString, err := myLocalDesires.GetDesire("SearchTerms") if (err != nil) { return false, 0, "", err } if (myDesireExists == false){ // No terms are selected return true, profileVersion, "0", nil } myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") myDesiredSearchTermsList := make([]string, 0, len(myDesiredChoicesList)) for _, searchTermBase64 := range myDesiredChoicesList{ searchTerm, err := encoding.DecodeBase64StringToUnicodeString(searchTermBase64) if (err != nil){ return false, 0, "", errors.New("My search term desires contains invalid term: " + searchTermBase64) } myDesiredSearchTermsList = append(myDesiredSearchTermsList, searchTerm) } // We count the number of occurances within the user's profile searchTermCount := 0 profileAttributesToCheckList := []string{"Description", "Tags", "Hobbies", "Beliefs"} for _, attributeName := range profileAttributesToCheckList{ attributeExists, _, attributeValue, err := getProfileAttributesFunction(attributeName) if (err != nil) { return false, 0, "", err } if (attributeExists == false){ continue } for _, searchTerm := range myDesiredSearchTermsList{ numberOfOccurances := strings.Count(attributeValue, searchTerm) searchTermCount += numberOfOccurances } } searchTermCountString := helpers.ConvertIntToString(searchTermCount) return true, profileVersion, searchTermCountString, nil } case "HasMessagedMe":{ userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } profileNetworkType, err := getUserProfileNetworkType() if (err != nil) { return false, 0, "", err } hasMessagedMe, err := myChatMessages.CheckIfUserHasMessagedMe(userIdentityHash, profileNetworkType) if (err != nil) { return false, 0, "", err } if (hasMessagedMe == false){ return true, profileVersion, "No", nil } return true, profileVersion, "Yes", nil } case "IHaveMessaged":{ userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } profileNetworkType, err := getUserProfileNetworkType() if (err != nil) { return false, 0, "", err } iHaveMessaged, err := myChatMessages.CheckIfIHaveMessagedUser(userIdentityHash, profileNetworkType) if (err != nil) { return false, 0, "", err } if (iHaveMessaged == false){ return true, profileVersion, "No", nil } return true, profileVersion, "Yes", nil } case "HasRejectedMe":{ if (profileType != "Mate"){ // Only Mate users can reject other users return true, profileVersion, "No", nil } myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(profileType) if (err != nil) { return false, 0, "", err } if (myIdentityExists == false){ return true, profileVersion, "No", nil } userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } profileNetworkType, err := getUserProfileNetworkType() if (err != nil) { return false, 0, "", err } myIdentityFound, anyMessagesFound, _, _, _, _, _, theyHaveContactedMe, theyHaveRejectedMe, _, err := myChatMessages.GetMyConversationInfoAndSortedMessagesList(myIdentityHash, userIdentityHash, profileNetworkType) if (err != nil) { return false, 0, "", err } if (myIdentityFound == false){ return false, 0, "", errors.New("My Identity not found after being found already.") } if (anyMessagesFound == false){ // No messages exist between us return true, profileVersion, "No", nil } if (theyHaveContactedMe == true && theyHaveRejectedMe == true){ return true, profileVersion, "Yes", nil } return true, profileVersion, "No", nil } case "IsLiked":{ userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } isLiked, _, err := myLikedUsers.CheckIfUserIsLiked(userIdentityHash) if (err != nil) { return false, 0, "", err } if (isLiked == true){ return true, profileVersion, "Yes", nil } return true, profileVersion, "No", nil } case "IsIgnored":{ userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } isIgnored, _, _, _, err := myIgnoredUsers.CheckIfUserIsIgnored(userIdentityHash) if (err != nil) { return false, 0, "", err } if (isIgnored == true){ return true, profileVersion, "Yes", nil } return true, profileVersion, "No", nil } case "IsMyContact":{ userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } isMyContact, err := myContacts.CheckIfUserIsMyContact(userIdentityHash) if (err != nil) { return false, 0, "", err } if (isMyContact == true){ return true, profileVersion, "Yes", nil } return true, profileVersion, "No", nil } case "WealthInGold":{ wealthExists, _, userWealth, err := getProfileAttributesFunction("Wealth") if (err != nil) { return false, 0, "", err } if (wealthExists == false){ return false, 0, "", nil } currencyExists, _, userCurrency, err := getProfileAttributesFunction("WealthCurrency") if (err != nil) { return false, 0, "", err } if (currencyExists == false){ return false, 0, "", errors.New("Database corrupt: Contains mate profile with Wealth but missing WealthCurrency") } userWealthFloat64, err := helpers.ConvertStringToFloat64(userWealth) if (err != nil){ return false, 0, "", errors.New("Database corrupt: Contains mate profile with invalid wealth: " + userWealth) } profileNetworkType, err := getUserProfileNetworkType() if (err != nil) { return false, 0, "", err } _, wealthInGold, err := convertCurrencies.ConvertCurrencyToKilogramsOfGold(profileNetworkType, userCurrency, userWealthFloat64) if (err != nil){ return false, 0, "", err } wealthInGoldString := helpers.ConvertFloat64ToString(wealthInGold) return true, profileVersion, wealthInGoldString, nil } case "RacialSimilarity":{ // Calculating racial similarity requires retrieving other calculated attributes // We have to recursively call this function getProfileAttributesFunctionForRacialSimilarity := func(inputAttributeName string)(bool, int, string, error){ // We make sure infinite recursion will not happen accidentally if (inputAttributeName == "RacialSimilarity"){ return false, 0, "", errors.New("Infinite recursion occurs during racial similarity attribute retrieval.") } exists, profileVersion, attributeValue, err := getProfileAttributesFunction(inputAttributeName) return exists, profileVersion, attributeValue, err } // RacialSimilarity is a value which aims to represent how racially similar 2 users are // // The calculation currently only adds points when the user has shared racial information such as genes and ancestry // If users have not shared their genes, they may still be racially similar // We want to prevent users who have not shared genes from being ranked too low by users who are sorting by racial similarity // We should accomplish this by deducting points if similarity is low, and adding points if similarity exists // Fine-tuning the algorithm requires real-world testing // We can see how well the algorithm is working by creating fake profiles with real photos of people along with their genomes racialSimilarity := float64(0) anyValueIsKnown := false addSimilarityAttributeToTotal := func(similarityAttributeName string, importanceFactor int)error{ valueIsKnown, _, similarityValueString, err := GetAnyProfileAttributeIncludingCalculated(similarityAttributeName, getProfileAttributesFunctionForRacialSimilarity) if (err != nil) { return err } if (valueIsKnown == true){ anyValueIsKnown = true // The input attribute value is a percentage between 0 - 100 similarityValue, err := helpers.ConvertStringToFloat64(similarityValueString) if (err != nil){ return errors.New("GetAnyProfileAttributeIncludingCalculated returning invalid " + similarityAttributeName + ": " + similarityValueString) } valueToAdd := similarityValue * float64(importanceFactor) racialSimilarity += valueToAdd } return nil } err = addSimilarityAttributeToTotal("EyeColorSimilarity", 4) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("EyeColorGenesSimilarity", 4) if (err != nil) { return false, 0, "", err } err := addSimilarityAttributeToTotal("HairColorSimilarity", 4) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("HairColorGenesSimilarity", 4) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("SkinColorSimilarity", 4) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("SkinColorGenesSimilarity", 4) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("HairTextureSimilarity", 2) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("HairTextureGenesSimilarity", 2) if (err != nil) { return false, 0, "", err } // TODO: Facial structure similarity (Comparison between user profile photos is needed) err = addSimilarityAttributeToTotal("FacialStructureGenesSimilarity", 3) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("23andMe_AncestralSimilarity", 5) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("23andMe_MaternalHaplogroupSimilarity", 1) if (err != nil) { return false, 0, "", err } err = addSimilarityAttributeToTotal("23andMe_PaternalHaplogroupSimilarity", 1) if (err != nil) { return false, 0, "", err } if (anyValueIsKnown == false){ return false, profileVersion, "", nil } racialSimilarityInt, err := helpers.FloorFloat64ToInt(racialSimilarity) if (err != nil) { return false, 0, "", err } racialSimilarityString := helpers.ConvertIntToString(racialSimilarityInt) return true, profileVersion, racialSimilarityString, nil } case "HairColorSimilarity":{ // HairColor is a "+" delimited string consisting of "Brown", "Black", "Blonde", "Orange" // The list must contain at least 1 color and cannot contain more than 2 colors. Repeats are not allowed. myHairColorExists, myHairColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "HairColor") if (err != nil) { return false, 0, "", err } if (myHairColorExists == false){ return false, 0, "", nil } userHairColorExists, _, userHairColorAttribute, err := getProfileAttributesFunction("HairColor") if (err != nil) { return false, 0, "", err } if (userHairColorExists == false){ return false, 0, "", nil } if (myHairColorAttribute == userHairColorAttribute){ return true, profileVersion, "100", nil } myHairColorList := strings.Split(myHairColorAttribute, "+") userHairColorList := strings.Split(userHairColorAttribute, "+") areEqual := helpers.CheckIfTwoListsContainIdenticalItems(myHairColorList, userHairColorList) if (areEqual == true){ return true, profileVersion, "100", nil } // Lists are not equal // 1 shared color == 50% similar. // No shared colors == 0% similar. for _, colorName := range myHairColorList{ containsColor := slices.Contains(userHairColorList, colorName) if (containsColor == true){ return true, profileVersion, "50", nil } } return true, profileVersion, "0", nil } case "SkinColorSimilarity":{ // Skin color is an integer between 1-6 mySkinColorExists, mySkinColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "SkinColor") if (err != nil) { return false, 0, "", err } if (mySkinColorExists == false){ return false, 0, "", nil } userSkinColorExists, _, userSkinColorAttribute, err := getProfileAttributesFunction("SkinColor") if (err != nil) { return false, 0, "", err } if (userSkinColorExists == false){ return false, 0, "", nil } mySkinColorInt, err := helpers.ConvertStringToInt(mySkinColorAttribute) if (err != nil){ return false, 0, "", errors.New("myLocalProfiles contains invalid SkinColor: " + mySkinColorAttribute) } userSkinColorInt, err := helpers.ConvertStringToInt(userSkinColorAttribute) if (err != nil){ return false, 0, "", errors.New("User profile contains invalid SkinColor: " + userSkinColorAttribute) } getSkinColorSimilarity := func()int{ if (mySkinColorInt == userSkinColorInt){ return 100 } lesserValue := min(mySkinColorInt, userSkinColorInt) greaterValue := max(mySkinColorInt, userSkinColorInt) difference := greaterValue - lesserValue if (difference == 1){ return 80 } else if (difference == 2){ return 60 } else if (difference == 3){ return 20 } return 0 } skinColorSimilarity := getSkinColorSimilarity() skinColorSimilarityString := helpers.ConvertIntToString(skinColorSimilarity) return true, profileVersion, skinColorSimilarityString, nil } case "EyeColorSimilarity":{ // The EyeColor attribute is a "+" delimited string consisting of: "Blue", "Green", "Hazel", Brown" // It must contain at least 1 color, cannot contain more than 4 colors. Repeats are not allowed. // Example: "Blue+Green", "Blue" myEyeColorExists, myEyeColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "EyeColor") if (err != nil) { return false, 0, "", err } if (myEyeColorExists == false){ return false, 0, "", nil } userEyeColorExists, _, userEyeColorAttribute, err := getProfileAttributesFunction("EyeColor") if (err != nil) { return false, 0, "", err } if (userEyeColorExists == false){ return false, 0, "", nil } getEyeColorSimilarity := func()string{ if (myEyeColorAttribute == userEyeColorAttribute){ return "100" } myEyeColorAttributeList := strings.Split(myEyeColorAttribute, "+") userEyeColorAttributeList := strings.Split(userEyeColorAttribute, "+") listsContainIdenticalItems := helpers.CheckIfTwoListsContainIdenticalItems(myEyeColorAttributeList, userEyeColorAttributeList) if (listsContainIdenticalItems == true){ return "100" } // This method could possibly be improved. numberOfSharedColors := 0 for _, colorName := range myEyeColorAttributeList{ containsColor := slices.Contains(userEyeColorAttributeList, colorName) if (containsColor == true){ numberOfSharedColors += 1 } } if (numberOfSharedColors == 0){ return "0" } if (numberOfSharedColors == 1){ return "50" } if (numberOfSharedColors == 2){ return "75" } // numberOfSharedColors == 3 // numberOfSharedColors cannot be 4 // If it was, then the lists would contain identical items, which we already checked for return "90" } eyeColorSimilarity := getEyeColorSimilarity() return true, profileVersion, eyeColorSimilarity, nil } case "HairTextureSimilarity":{ // Hair Texture is an integer between 1-6 myHairTextureExists, myHairTextureAttribute, err := myLocalProfiles.GetProfileData("Mate", "HairTexture") if (err != nil) { return false, 0, "", err } if (myHairTextureExists == false){ return false, 0, "", nil } userHairTextureExists, _, userHairTextureAttribute, err := getProfileAttributesFunction("HairTexture") if (err != nil) { return false, 0, "", err } if (userHairTextureExists == false){ return false, 0, "", nil } myHairTextureInt, err := helpers.ConvertStringToInt(myHairTextureAttribute) if (err != nil){ return false, 0, "", errors.New("myLocalProfiles contains invalid HairTexture: " + myHairTextureAttribute) } userHairTextureInt, err := helpers.ConvertStringToInt(userHairTextureAttribute) if (err != nil){ return false, 0, "", errors.New("User profile contains invalid HairTexture: " + userHairTextureAttribute) } getHairTextureSimilarity := func()int{ if (myHairTextureInt == userHairTextureInt){ return 100 } lesserValue := min(myHairTextureInt, userHairTextureInt) greaterValue := max(myHairTextureInt, userHairTextureInt) difference := greaterValue - lesserValue if (difference == 1){ return 80 } else if (difference == 2){ return 70 } else if (difference == 3){ return 20 } return 0 } hairTextureSimilarity := getHairTextureSimilarity() hairTextureSimilarityString := helpers.ConvertIntToString(hairTextureSimilarity) return true, profileVersion, hairTextureSimilarityString, nil } case "EyeColorGenesSimilarity", "EyeColorGenesSimilarity_NumberOfSimilarAlleles", "HairColorGenesSimilarity", "HairColorGenesSimilarity_NumberOfSimilarAlleles", "SkinColorGenesSimilarity", "SkinColorGenesSimilarity_NumberOfSimilarAlleles", "HairTextureGenesSimilarity", "HairTextureGenesSimilarity_NumberOfSimilarAlleles", "FacialStructureGenesSimilarity", "FacialStructureGenesSimilarity_NumberOfSimilarAlleles":{ // Disclaimer: I'm not sure how well this comparison will work // TODO Compare magnitude of rsIDs, because some rsIDs are more causal than others. // We may want to also calculate the outcomes for each user's genes, not just the raw genes. // That calcuation would be a lot slower. // // We want this similarity comparison to aid users in their search for mates who look like them and with whom // they are likely to produce offspring who look like them 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 genetic information is unknown return false, profileVersion, "", nil } getTraitName := func()string{ isEyeColor := strings.HasPrefix(attributeName, "EyeColor") if (isEyeColor == true){ return "Eye Color" } isHairColor := strings.HasPrefix(attributeName, "HairColor") if (isHairColor == true){ return "Hair Color" } isSkinColor := strings.HasPrefix(attributeName, "SkinColor") if (isSkinColor == true){ return "Skin Color" } isHairTexture := strings.HasPrefix(attributeName, "HairTexture") if (isHairTexture == true){ return "Hair Texture" } // attributeName prefix == "FacialStructure" return "Facial Structure" } traitName := getTraitName() _, _, _, _, 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 } traitLociList := traitObject.LociList // This keeps track of the number of alleles for which both us and the user have a known value numberOfKnownAlleles := 0 // This keeps track of the number of alleles for which us and the user have the same value numberOfSimilarAlleles := 0 for _, rsID := range traitLociList{ myLocusValue, myLocusValueExists := myGenomeLocusValuesMap[rsID] if (myLocusValueExists == false){ continue } rsIDString := helpers.ConvertInt64ToString(rsID) userLocusValueExists, _, userLocusValue, err := getProfileAttributesFunction("LocusValue_rs" + rsIDString) if (err != nil) { return false, 0, "", err } if (userLocusValueExists == false){ continue } numberOfKnownAlleles += 2 //TODO: Deal with locus base pair phase information // We may want to require all loci to be phased for the comparison // Users who want to use this feature would have to get a genetic sequence which has phase information // This would save space on profiles because we wouldn't have to share an IsPhased bool for each locus (because all loci would be phased) // People who are more knowledgeable about genetics should share their opinions. myLocusValueBase1 := myLocusValue.Base1Value myLocusValueBase2 := myLocusValue.Base2Value userLocusValueBase1, userLocusValueBase2, semicolonExists := strings.Cut(userLocusValue, ";") if (semicolonExists == false){ return false, 0, "", errors.New("Database contains invalid profile: Profile contains invalid LocusValue_rs" + rsIDString + " value: " + userLocusValue) } // We are counting how many shared alleles we have for this locus, irrespective of locus phase // // For example: // // User1: TT, User2: GT = 1 shared allele // User1: AG, User2: GA = 2 shared alleles // User1: CC, User2: GG = 0 shared alleles // // We will possibly change this strategy once we include locus phase information. if (myLocusValueBase1 == userLocusValueBase1){ numberOfSimilarAlleles += 1 if (myLocusValueBase2 == userLocusValueBase2){ numberOfSimilarAlleles += 1 } continue } if (myLocusValueBase1 == userLocusValueBase2){ numberOfSimilarAlleles += 1 if (myLocusValueBase2 == userLocusValueBase1){ numberOfSimilarAlleles += 1 } continue } if (myLocusValueBase2 == userLocusValueBase1){ numberOfSimilarAlleles += 1 // We already know that myLocusValueBase1 != userLocusValueBase2 continue } if (myLocusValueBase2 == userLocusValueBase2){ numberOfSimilarAlleles += 1 // We already know that myLocusValueBase1 != userLocusValueBase1 continue } } if (numberOfKnownAlleles < 15){ // We don't know enough loci values to be able to calculate genetic similarity return false, 0, "", nil } attributeIsNumberOfSimilarAlleles := strings.HasSuffix(attributeName, "_NumberOfSimilarAlleles") if (attributeIsNumberOfSimilarAlleles == true){ numberOfSimilarAllelesString := helpers.ConvertIntToString(numberOfSimilarAlleles) numberOfKnownAllelesString := helpers.ConvertIntToString(numberOfKnownAlleles) result := numberOfSimilarAllelesString + "/" + numberOfKnownAllelesString return true, profileVersion, result, nil } genesSimilarity := (float64(numberOfSimilarAlleles)/float64(numberOfKnownAlleles)) * 100 genesSimilarityInt, err := helpers.FloorFloat64ToInt(genesSimilarity) if (err != nil) { return false, 0, "", err } if (genesSimilarityInt > 100){ return true, profileVersion, "100", nil } genesSimilarityString := helpers.ConvertIntToString(genesSimilarityInt) return true, profileVersion, genesSimilarityString, nil } case "23andMe_AncestralSimilarity":{ // Ancestral similarity aims to compare how closely related a user's ancestors are. // Ancestral similarity could be improved by comparing categories based on their genetic distance. // If one pair of categories only diverged genetically 5,000 years ago, and the other diverged 30,000 years ago, // we would count the more recently diverged group as being more similar. // This may not always be the best measurement, because we must also take into account the genetic // similarity between different groups. // For example, a population which diverged a longer time ago might still be more similar due to factors // such as gene flow and a more similar evolution. // People who are more knowledgeable about these topics should share their thoughts. myAncestryCompositionExists, myAncestryCompositionAttribute, err := myLocalProfiles.GetProfileData("Mate", "23andMe_AncestryComposition") if (err != nil){ return false, 0, "", err } if (myAncestryCompositionExists == false){ return false, 0, "", nil } userAncestryCompositionExists, _, userAncestryCompositionAttribute, err := getProfileAttributesFunction("23andMe_AncestryComposition") if (err != nil) { return false, 0, "", err } if (userAncestryCompositionExists == false){ return false, 0, "", nil } ancestralSimilarity, err := companyAnalysis.GetAncestralSimilarity_23andMe(false, myAncestryCompositionAttribute, userAncestryCompositionAttribute) if (err != nil){ return false, 0, "", errors.New("GetAncestralSimilarity_23andMe failed when calculating my 23andMe_AncestralSimilarity with another user: " + err.Error()) } ancestralSimilarityString := helpers.ConvertIntToString(ancestralSimilarity) return true, profileVersion, ancestralSimilarityString, nil } case "23andMe_MaternalHaplogroupSimilarity", "23andMe_PaternalHaplogroupSimilarity":{ profileAttributeName := strings.TrimSuffix(attributeName, "Similarity") myHaplogroupExists, myHaplogroup, err := myLocalProfiles.GetProfileData("Mate", profileAttributeName) if (err != nil){ return false, 0, "", err } if (myHaplogroupExists == false){ return false, 0, "", nil } userHaplogroupExists, _, userHaplogroup, err := getProfileAttributesFunction(profileAttributeName) if (err != nil){ return false, 0, "", err } if (userHaplogroupExists == false){ return false, 0, "", nil } //TODO: Rank haplogroups based on genetic similarity and incorporate into calculation if (myHaplogroup == userHaplogroup){ return true, profileVersion, "100", nil } return true, profileVersion, "0", nil } case "NumberOfReviews":{ if (profileType != "Moderator"){ return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with NumberOfReviews attribute for non-Moderator profileType: " + profileType) } userIdentityHash, err := getUserIdentityHash() if (err != nil) { return false, 0, "", err } numberOfReviews := 0 anyExist, identityReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Identity") if (err != nil) { return false, 0, "", err } if (anyExist == true){ numberOfReviews += len(identityReviewHashesList) } anyExist, profileReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Profile") if (err != nil) { return false, 0, "", err } if (anyExist == true){ numberOfReviews += len(profileReviewHashesList) } anyExist, attributeReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Attribute") if (err != nil) { return false, 0, "", err } if (anyExist == true){ numberOfReviews += len(attributeReviewHashesList) } anyExist, messageReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Message") if (err != nil) { return false, 0, "", err } if (anyExist == true){ numberOfReviews += len(messageReviewHashesList) } numberOfReviewsString := helpers.ConvertIntToString(numberOfReviews) return true, profileVersion, numberOfReviewsString, nil } } return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with unknown attribute: " + attributeName) }