seekia/internal/profiles/calculatedAttributes/calculatedAttributes.go

1655 lines
62 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 _, locusObject := range diseaseLociList{
locusRSID := locusObject.LocusRSID
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
}
anyLocusTested, userDiseaseRiskScore, _, _, err := createPersonGeneticAnalysis.GetPersonGenomePolygenicDiseaseInfo(diseaseLociList, userDiseaseLocusValuesMap, true)
if (err != nil) { return false, 0, "", err }
if (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 _, locusObject := range diseaseLociList{
locusRSID := locusObject.LocusRSID
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
}
anyLocusValuesTested, offspringAverageRiskScore, _, err := createCoupleGeneticAnalysis.GetOffspringPolygenicDiseaseInfo_Fast(diseaseLociList, myGenomeLocusValuesMap, userDiseaseLocusValuesMap)
if (err != nil) { return false, 0, "", err }
if (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)
}