seekia/internal/profiles/calculatedAttributes/calculatedAttributes.go

1652 lines
62 KiB
Go
Raw Normal View History

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