2024-04-11 15:51:56 +02:00
// 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"
2024-06-15 02:43:01 +02:00
import "seekia/internal/genetics/createCoupleGeneticAnalysis"
import "seekia/internal/genetics/createPersonGeneticAnalysis"
2024-06-07 02:04:13 +02:00
import "seekia/internal/genetics/locusValue"
2024-04-11 15:51:56 +02:00
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
2024-06-02 10:43:39 +02:00
myPersonChosen , myGenomesExist , myAnalysisIsReady , myGeneticAnalysisObject , myGenomeIdentifier , _ , err := myChosenAnalysis . GetMyChosenMateGeneticAnalysis ( )
2024-04-11 15:51:56 +02:00
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
2024-06-02 10:43:39 +02:00
// This stores the probability of the offspring having each tested disease
// Each float in this list is a value between 0-1
2024-04-11 15:51:56 +02:00
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
}
2024-06-02 10:43:39 +02:00
myProbabilityIsKnown , _ , myProbabilityOfPassingADiseaseVariant , _ , _ , _ , _ , _ , err := readGeneticAnalysis . GetPersonMonogenicDiseaseInfoFromGeneticAnalysis ( myGeneticAnalysisObject , monogenicDiseaseName , myGenomeIdentifier )
2024-04-11 15:51:56 +02:00
if ( err != nil ) { return false , 0 , "" , err }
if ( userProbabilityIsKnown == false && myProbabilityIsKnown == false ) {
continue
}
2024-06-02 10:43:39 +02:00
getUserProbabilityOfPassingADiseaseVariantInt := func ( ) ( int , error ) {
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
if ( userProbabilityIsKnown == false ) {
return 0 , nil
}
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
userProbabilityOfPassingADiseaseVariantInt , err := helpers . ConvertStringToInt ( userProbabilityOfPassingADiseaseVariant )
if ( err != nil ) {
return 0 , errors . New ( "GetAnyProfileAttributeIncludingCalculated called with profile containing invalid " + probabilityOfPassingAVariantAttributeName + ": " + userProbabilityOfPassingADiseaseVariant )
}
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
return userProbabilityOfPassingADiseaseVariantInt , nil
}
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
userProbabilityOfPassingADiseaseVariantInt , err := getUserProbabilityOfPassingADiseaseVariantInt ( )
if ( err != nil ) { return false , 0 , "" , err }
2024-04-11 15:51:56 +02:00
2024-06-15 02:43:01 +02:00
probabilityOffspringHasDiseaseIsKnown , offspringPercentageProbabilityOfDisease , _ , _ , err := createCoupleGeneticAnalysis . GetOffspringMonogenicDiseaseProbabilities ( diseaseIsDominantOrRecessive , myProbabilityIsKnown , myProbabilityOfPassingADiseaseVariant , userProbabilityIsKnown , userProbabilityOfPassingADiseaseVariantInt )
2024-06-02 10:43:39 +02:00
if ( err != nil ) { return false , 0 , "" , err }
if ( probabilityOffspringHasDiseaseIsKnown == false ) {
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
// 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
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
getNonZeroUnknownDiseaseRiskExistsBool := func ( ) bool {
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
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
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
return true
2024-04-11 15:51:56 +02:00
}
2024-06-02 10:43:39 +02:00
// diseaseIsDominantOrRecessive == "Dominant"
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
if ( userProbabilityIsKnown == true && userProbabilityOfPassingADiseaseVariantInt != 0 ) {
return true
}
2024-04-11 15:51:56 +02:00
2024-06-02 10:43:39 +02:00
if ( myProbabilityIsKnown == true && myProbabilityOfPassingADiseaseVariant != 0 ) {
return true
2024-04-11 15:51:56 +02:00
}
2024-06-02 10:43:39 +02:00
return false
2024-04-11 15:51:56 +02:00
}
2024-06-02 10:43:39 +02:00
nonZeroUnknownDiseaseRiskExists = getNonZeroUnknownDiseaseRiskExistsBool ( )
2024-04-11 15:51:56 +02:00
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
}
2024-06-02 10:43:39 +02:00
if ( offspringPercentageProbabilityOfDisease == 100 ) {
2024-04-11 15:51:56 +02:00
// Probability that offspring will have a disease is 100%
return true , profileVersion , "100" , nil
}
2024-06-02 10:43:39 +02:00
offspringProbabilityOfDisease := float64 ( offspringPercentageProbabilityOfDisease ) / float64 ( 100 )
2024-04-11 15:51:56 +02:00
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 {
2024-06-02 10:43:39 +02:00
if ( diseaseProbability < 0 || diseaseProbability > 1 ) {
return false , 0 , "" , errors . New ( "allDiseaseProbabilitiesList contains invalid disease probability." )
}
2024-04-11 15:51:56 +02:00
// We multiply by the probability of no disease
2024-06-02 10:43:39 +02:00
probabilityOfNoMonogenicDiseases *= ( 1 - diseaseProbability )
2024-04-11 15:51:56 +02:00
}
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 {
2024-06-07 02:04:13 +02:00
// Map Structure: Locus rsID -> Locus Value
userDiseaseLocusValuesMap := make ( map [ int64 ] locusValue . LocusValue )
2024-04-11 15:51:56 +02:00
2024-06-07 02:04:13 +02:00
diseaseLociList := diseaseObject . LociList
2024-04-11 15:51:56 +02:00
for _ , locusObject := range diseaseLociList {
locusRSID := locusObject . LocusRSID
locusRSIDString := helpers . ConvertInt64ToString ( locusRSID )
2024-06-07 02:04:13 +02:00
locusValueAttributeName := "LocusValue_rs" + locusRSIDString
2024-04-11 15:51:56 +02:00
2024-08-05 09:11:10 +02:00
userLocusValueExists , _ , userLocusValue , err := getProfileAttributesFunction ( locusValueAttributeName )
2024-04-11 15:51:56 +02:00
if ( err != nil ) { return false , 0 , "" , err }
2024-08-05 09:11:10 +02:00
if ( userLocusValueExists == false ) {
2024-04-11 15:51:56 +02:00
continue
}
2024-08-05 09:11:10 +02:00
userLocusBase1 , userLocusBase2 , semicolonFound := strings . Cut ( userLocusValue , ";" )
2024-06-07 02:04:13 +02:00
if ( semicolonFound == false ) {
2024-08-05 09:11:10 +02:00
return false , 0 , "" , errors . New ( "Database corrupt: Contains profile with invalid " + locusValueAttributeName + " value: " + userLocusValue )
2024-06-07 02:04:13 +02:00
}
2024-04-11 15:51:56 +02:00
2024-08-05 09:11:10 +02:00
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 {
2024-06-07 02:04:13 +02:00
Base1Value : userLocusBase1 ,
Base2Value : userLocusBase2 ,
2024-08-05 09:11:10 +02:00
LocusIsPhased : userLocusIsPhased ,
2024-04-11 15:51:56 +02:00
}
2024-06-07 02:04:13 +02:00
2024-08-05 09:11:10 +02:00
userDiseaseLocusValuesMap [ locusRSID ] = userLocusValueObject
2024-04-11 15:51:56 +02:00
}
2024-06-15 02:43:01 +02:00
anyLocusTested , userDiseaseRiskScore , _ , _ , err := createPersonGeneticAnalysis . GetPersonGenomePolygenicDiseaseInfo ( diseaseLociList , userDiseaseLocusValuesMap , true )
2024-06-07 02:04:13 +02:00
if ( err != nil ) { return false , 0 , "" , err }
if ( anyLocusTested == false ) {
2024-04-11 15:51:56 +02:00
continue
}
numberOfDiseasesTested += 1
2024-06-07 02:04:13 +02:00
userRiskScoreFraction := float64 ( userDiseaseRiskScore ) / float64 ( 10 )
2024-04-11 15:51:56 +02:00
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
2024-06-02 10:43:39 +02:00
myPersonChosen , myGenomesExist , myAnalysisIsReady , myGeneticAnalysisObject , myGenomeIdentifier , _ , err := myChosenAnalysis . GetMyChosenMateGeneticAnalysis ( )
2024-04-11 15:51:56 +02:00
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
}
2024-07-19 19:16:28 +02:00
_ , _ , _ , _ , 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." )
}
2024-04-11 15:51:56 +02:00
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
2024-06-07 02:04:13 +02:00
// Map Structure: rsID -> Locus Value
userDiseaseLocusValuesMap := make ( map [ int64 ] locusValue . LocusValue )
2024-06-02 10:43:39 +02:00
2024-06-07 02:04:13 +02:00
for _ , locusObject := range diseaseLociList {
2024-06-02 10:43:39 +02:00
2024-04-11 15:51:56 +02:00
locusRSID := locusObject . LocusRSID
locusRSIDString := helpers . ConvertInt64ToString ( locusRSID )
locusValueAttributeName := "LocusValue_rs" + locusRSIDString
2024-08-05 09:11:10 +02:00
userLocusValueExists , _ , userLocusValue , err := getProfileAttributesFunction ( locusValueAttributeName )
2024-04-11 15:51:56 +02:00
if ( err != nil ) { return false , 0 , "" , err }
2024-08-05 09:11:10 +02:00
if ( userLocusValueExists == false ) {
2024-04-11 15:51:56 +02:00
continue
}
2024-08-05 09:11:10 +02:00
userLocusBase1 , userLocusBase2 , semicolonFound := strings . Cut ( userLocusValue , ";" )
2024-06-02 10:43:39 +02:00
if ( semicolonFound == false ) {
2024-08-05 09:11:10 +02:00
return false , 0 , "" , errors . New ( "GetAnyProfileAttributeIncludingCalculated called with profile containing invalid " + locusValueAttributeName + ": " + userLocusValue )
2024-06-02 10:43:39 +02:00
}
2024-08-05 09:11:10 +02:00
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 {
2024-06-07 02:04:13 +02:00
Base1Value : userLocusBase1 ,
Base2Value : userLocusBase2 ,
2024-08-05 09:11:10 +02:00
LocusIsPhased : userLocusIsPhased ,
2024-06-07 02:04:13 +02:00
}
2024-08-05 09:11:10 +02:00
userDiseaseLocusValuesMap [ locusRSID ] = newLocusValueObject
2024-04-11 15:51:56 +02:00
}
2024-07-19 19:16:28 +02:00
anyLocusValuesTested , offspringAverageRiskScore , _ , err := createCoupleGeneticAnalysis . GetOffspringPolygenicDiseaseInfo_Fast ( diseaseLociList , myGenomeLocusValuesMap , userDiseaseLocusValuesMap )
2024-06-07 02:04:13 +02:00
if ( err != nil ) { return false , 0 , "" , err }
if ( anyLocusValuesTested == false ) {
2024-04-11 15:51:56 +02:00
continue
}
numberOfDiseasesTested += 1
2024-06-07 02:04:13 +02:00
offspringRiskScoreFraction := float64 ( offspringAverageRiskScore ) / float64 ( 10 )
2024-04-11 15:51:56 +02:00
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
2024-06-02 10:43:39 +02:00
myPersonChosen , myGenomesExist , myAnalysisIsReady , myGeneticAnalysisObject , myGenomeIdentifier , _ , err := myChosenAnalysis . GetMyChosenMateGeneticAnalysis ( )
2024-04-11 15:51:56 +02:00
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 ( )
2024-07-19 19:16:28 +02:00
_ , _ , _ , _ , myGenomesMap , err := readGeneticAnalysis . GetMetadataFromPersonGeneticAnalysis ( myGeneticAnalysisObject )
2024-04-11 15:51:56 +02:00
if ( err != nil ) { return false , 0 , "" , err }
2024-07-19 19:16:28 +02:00
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." )
}
2024-04-11 15:51:56 +02:00
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 {
2024-07-19 19:16:28 +02:00
myLocusValue , myLocusValueExists := myGenomeLocusValuesMap [ rsID ]
2024-04-11 15:51:56 +02:00
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.
2024-06-02 10:43:39 +02:00
myLocusValueBase1 := myLocusValue . Base1Value
myLocusValueBase2 := myLocusValue . Base2Value
2024-04-11 15:51:56 +02:00
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 )
}