package persona import ( "fmt" "math" ) // PlausibilityLevel represents how biologically plausible a feature combination is. type PlausibilityLevel string const ( PlausibilityHighlyPlausible PlausibilityLevel = "highly_plausible" // 50%+ probability PlausibilityPlausible PlausibilityLevel = "plausible" // 15-50% probability PlausibilityUnusual PlausibilityLevel = "unusual" // 5-15% probability PlausibilityRare PlausibilityLevel = "rare" // 1-5% probability PlausibilityImplausible PlausibilityLevel = "implausible" // < 1% probability ) // PlausibilityViolation represents a single biological plausibility issue. type PlausibilityViolation struct { Field string // e.g., "face.eye_color" Value interface{} // The actual value Ethnicity EthnicityCode // The ethnicity being validated against Level PlausibilityLevel // How plausible this is Probability float64 // Actual probability (0.0-1.0) Message string // Human-readable explanation } // PlausibilityResult contains the validation results for DNA. type PlausibilityResult struct { Valid bool // True if no implausible violations Violations []PlausibilityViolation // All violations found Score float64 // Overall plausibility score (0-1) } // ValidateHumanPlausibility checks if face/hair features are biologically plausible // for the given ethnicity. Returns violations for implausible combinations. func ValidateHumanPlausibility(dna *DNA) *PlausibilityResult { if dna == nil { return &PlausibilityResult{ Valid: true, Score: 1.0, } } result := &PlausibilityResult{ Valid: true, Violations: []PlausibilityViolation{}, Score: 1.0, } ethnicity := dna.Identity.Ethnicity if ethnicity == "" || ethnicity == EthnicityMixed { // Mixed or unspecified ethnicity - skip plausibility checks return result } // Validate eye color if violation := validateEyeColor(dna.Face.EyeColor, ethnicity); violation != nil { result.Violations = append(result.Violations, *violation) if violation.Level == PlausibilityImplausible { result.Valid = false } } // Validate hair color if violation := validateHairColor(dna.Face.HairColor, ethnicity); violation != nil { result.Violations = append(result.Violations, *violation) if violation.Level == PlausibilityImplausible { result.Valid = false } } // Validate hair texture if violation := validateHairTexture(dna.Face.HairTexture, ethnicity); violation != nil { result.Violations = append(result.Violations, *violation) if violation.Level == PlausibilityImplausible { result.Valid = false } } // Validate skin tone if violation := validateSkinTone(dna.Face.SkinTone, ethnicity); violation != nil { result.Violations = append(result.Violations, *violation) if violation.Level == PlausibilityImplausible { result.Valid = false } } // Calculate overall plausibility score (geometric mean of probabilities) if len(result.Violations) > 0 { product := 1.0 for _, v := range result.Violations { product *= v.Probability } numFeatures := 4.0 // eye_color, hair_color, hair_texture, skin_tone result.Score = math.Pow(product, 1.0/numFeatures) } return result } // validateEyeColor checks if eye color is plausible for the ethnicity. func validateEyeColor(eyeColor EyeColorCategory, ethnicity EthnicityCode) *PlausibilityViolation { if eyeColor == "" { return nil } prob := getEyeColorProbability(eyeColor, ethnicity) level := getProbabilityLevel(prob) if level == PlausibilityUnusual || level == PlausibilityRare || level == PlausibilityImplausible { return &PlausibilityViolation{ Field: "face.eye_color", Value: eyeColor, Ethnicity: ethnicity, Level: level, Probability: prob, Message: fmt.Sprintf("Eye color %s is %s for %s ethnicity (%.1f%% probability)", eyeColor, level, ethnicity, prob*100), } } return nil } // validateHairColor checks if hair color is plausible for the ethnicity. func validateHairColor(hairColor HairColorCategory, ethnicity EthnicityCode) *PlausibilityViolation { if hairColor == "" { return nil } prob := getHairColorProbability(hairColor, ethnicity) level := getProbabilityLevel(prob) if level == PlausibilityUnusual || level == PlausibilityRare || level == PlausibilityImplausible { return &PlausibilityViolation{ Field: "face.hair_color", Value: hairColor, Ethnicity: ethnicity, Level: level, Probability: prob, Message: fmt.Sprintf("Hair color %s is %s for %s ethnicity (%.1f%% probability)", hairColor, level, ethnicity, prob*100), } } return nil } // validateHairTexture checks if hair texture is plausible for the ethnicity. func validateHairTexture(hairTexture HairTextureCategory, ethnicity EthnicityCode) *PlausibilityViolation { if hairTexture == "" { return nil } prob := getHairTextureProbability(hairTexture, ethnicity) level := getProbabilityLevel(prob) if level == PlausibilityUnusual || level == PlausibilityRare || level == PlausibilityImplausible { return &PlausibilityViolation{ Field: "face.hair_texture", Value: hairTexture, Ethnicity: ethnicity, Level: level, Probability: prob, Message: fmt.Sprintf("Hair texture %s is %s for %s ethnicity (%.1f%% probability)", hairTexture, level, ethnicity, prob*100), } } return nil } // validateSkinTone checks if skin tone is plausible for the ethnicity. func validateSkinTone(skinTone SkinToneCategory, ethnicity EthnicityCode) *PlausibilityViolation { if skinTone == "" { return nil } if !isSkinTonePlausible(skinTone, ethnicity) { return &PlausibilityViolation{ Field: "face.skin_tone", Value: skinTone, Ethnicity: ethnicity, Level: PlausibilityImplausible, Probability: 0.005, Message: fmt.Sprintf("Skin tone %s is implausible for %s ethnicity", skinTone, ethnicity), } } return nil } // getProbabilityLevel converts a probability to a PlausibilityLevel. func getProbabilityLevel(prob float64) PlausibilityLevel { if prob >= 0.5 { return PlausibilityHighlyPlausible } else if prob >= 0.15 { return PlausibilityPlausible } else if prob >= 0.05 { return PlausibilityUnusual } else if prob >= 0.01 { return PlausibilityRare } else { return PlausibilityImplausible } } // getEyeColorProbability returns the probability of an eye color for an ethnicity. func getEyeColorProbability(eyeColor EyeColorCategory, ethnicity EthnicityCode) float64 { switch ethnicity { case EthnicityEastAsian, EthnicitySoutheastAsian: switch eyeColor { case EyeColorDarkBrown: return 0.85 case EyeColorBrown: return 0.15 case EyeColorHazel: return 0.005 default: return 0.001 } case EthnicitySouthAsian: switch eyeColor { case EyeColorDarkBrown: return 0.75 case EyeColorBrown: return 0.20 case EyeColorHazel: return 0.03 case EyeColorAmber: return 0.02 default: return 0.005 } case EthnicityAfrican: switch eyeColor { case EyeColorDarkBrown: return 0.85 case EyeColorBrown: return 0.13 case EyeColorAmber: return 0.02 default: return 0.002 } case EthnicityHispanic: switch eyeColor { case EyeColorBrown: return 0.55 case EyeColorDarkBrown: return 0.25 case EyeColorHazel: return 0.12 case EyeColorGreen: return 0.05 case EyeColorAmber: return 0.03 default: return 0.01 } case EthnicityMiddleEastern: switch eyeColor { case EyeColorBrown: return 0.50 case EyeColorDarkBrown: return 0.30 case EyeColorHazel: return 0.10 case EyeColorGreen: return 0.07 case EyeColorAmber: return 0.03 default: return 0.005 } case EthnicityCaucasian: switch eyeColor { case EyeColorBlue: return 0.30 case EyeColorBrown: return 0.25 case EyeColorGreen: return 0.15 case EyeColorHazel: return 0.15 case EyeColorGray: return 0.10 case EyeColorDarkBrown: return 0.05 default: return 0.02 } default: return 0.15 // Mixed/unknown - moderate probability } } // getHairColorProbability returns the probability of a hair color for an ethnicity. func getHairColorProbability(hairColor HairColorCategory, ethnicity EthnicityCode) float64 { switch ethnicity { case EthnicityEastAsian, EthnicitySoutheastAsian: switch hairColor { case HairColorBlack: return 0.85 case HairColorDarkBrown: return 0.12 case HairColorBrown: return 0.03 default: return 0.001 } case EthnicitySouthAsian: switch hairColor { case HairColorBlack: return 0.85 case HairColorDarkBrown: return 0.12 case HairColorBrown: return 0.03 default: return 0.001 } case EthnicityAfrican: switch hairColor { case HairColorBlack: return 0.92 case HairColorDarkBrown: return 0.08 default: return 0.001 } case EthnicityHispanic: switch hairColor { case HairColorBlack: return 0.50 case HairColorDarkBrown: return 0.35 case HairColorBrown: return 0.12 case HairColorLightBrown: return 0.03 default: return 0.005 } case EthnicityMiddleEastern: switch hairColor { case HairColorBlack: return 0.65 case HairColorDarkBrown: return 0.30 case HairColorBrown: return 0.05 default: return 0.005 } case EthnicityCaucasian: switch hairColor { case HairColorBrown: return 0.35 case HairColorLightBrown: return 0.20 case HairColorBlonde: return 0.20 case HairColorDarkBrown: return 0.12 case HairColorAuburn: return 0.08 case HairColorRed: return 0.05 default: return 0.02 } default: return 0.10 } } // getHairTextureProbability returns the probability of a hair texture for an ethnicity. func getHairTextureProbability(hairTexture HairTextureCategory, ethnicity EthnicityCode) float64 { switch ethnicity { case EthnicityEastAsian, EthnicitySoutheastAsian: switch hairTexture { case HairTextureStraight: return 0.85 case HairTextureWavy: return 0.15 case HairTextureCurly: return 0.005 case HairTextureCoily, HairTextureKinky: return 0.001 default: return 0.01 } case EthnicitySouthAsian: switch hairTexture { case HairTextureWavy: return 0.40 case HairTextureStraight: return 0.35 case HairTextureCurly: return 0.25 default: return 0.01 } case EthnicityAfrican: switch hairTexture { case HairTextureCoily: return 0.50 case HairTextureKinky: return 0.30 case HairTextureCurly: return 0.20 case HairTextureWavy: return 0.005 case HairTextureStraight: return 0.001 default: return 0.01 } case EthnicityHispanic: switch hairTexture { case HairTextureWavy: return 0.40 case HairTextureStraight: return 0.30 case HairTextureCurly: return 0.30 default: return 0.01 } case EthnicityMiddleEastern: switch hairTexture { case HairTextureWavy: return 0.45 case HairTextureStraight: return 0.30 case HairTextureCurly: return 0.25 default: return 0.01 } case EthnicityCaucasian: switch hairTexture { case HairTextureStraight: return 0.40 case HairTextureWavy: return 0.40 case HairTextureCurly: return 0.20 default: return 0.01 } default: return 0.15 } } // isSkinTonePlausible checks if a skin tone is plausible for an ethnicity. func isSkinTonePlausible(skinTone SkinToneCategory, ethnicity EthnicityCode) bool { switch ethnicity { case EthnicityEastAsian, EthnicitySoutheastAsian: return skinTone == SkinToneFair || skinTone == SkinToneLight || skinTone == SkinToneMedium || skinTone == SkinToneTan case EthnicitySouthAsian: return skinTone == SkinToneLight || skinTone == SkinToneMedium || skinTone == SkinToneTan || skinTone == SkinToneBrown || skinTone == SkinToneDeep case EthnicityAfrican: return skinTone == SkinToneMedium || skinTone == SkinToneTan || skinTone == SkinToneBrown || skinTone == SkinToneDarkBrown || skinTone == SkinToneDeep case EthnicityHispanic: return skinTone == SkinToneLight || skinTone == SkinToneMedium || skinTone == SkinToneTan || skinTone == SkinToneBrown || skinTone == SkinToneDeep || skinTone == SkinToneOlive case EthnicityMiddleEastern: return skinTone == SkinToneLight || skinTone == SkinToneMedium || skinTone == SkinToneTan || skinTone == SkinToneOlive case EthnicityCaucasian: return skinTone == SkinToneFair || skinTone == SkinToneLight || skinTone == SkinToneMedium case EthnicityMixed: return true // Mixed allows all skin tones default: return true // Unknown allows all } }