use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::health::{analyze_mthfr, analyze_pain};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BiomarkerReference {
pub name: &'static str,
pub unit: &'static str,
pub normal_low: f64,
pub normal_high: f64,
pub critical_low: Option<f64>,
pub critical_high: Option<f64>,
pub category: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BiomarkerClassification {
CriticalLow,
Low,
Normal,
High,
CriticalHigh,
}
static REFERENCES: &[BiomarkerReference] = &[
BiomarkerReference { name: "Total Cholesterol", unit: "mg/dL", normal_low: 125.0, normal_high: 200.0, critical_low: Some(100.0), critical_high: Some(300.0), category: "Lipid" },
BiomarkerReference { name: "LDL", unit: "mg/dL", normal_low: 50.0, normal_high: 100.0, critical_low: Some(25.0), critical_high: Some(190.0), category: "Lipid" },
BiomarkerReference { name: "HDL", unit: "mg/dL", normal_low: 40.0, normal_high: 90.0, critical_low: Some(20.0), critical_high: None, category: "Lipid" },
BiomarkerReference { name: "Triglycerides", unit: "mg/dL", normal_low: 35.0, normal_high: 150.0, critical_low: Some(20.0), critical_high: Some(500.0), category: "Lipid" },
BiomarkerReference { name: "Fasting Glucose", unit: "mg/dL", normal_low: 70.0, normal_high: 100.0, critical_low: Some(50.0), critical_high: Some(250.0), category: "Metabolic" },
BiomarkerReference { name: "HbA1c", unit: "%", normal_low: 4.0, normal_high: 5.7, critical_low: None, critical_high: Some(9.0), category: "Metabolic" },
BiomarkerReference { name: "Homocysteine", unit: "umol/L", normal_low: 5.0, normal_high: 15.0, critical_low: None, critical_high: Some(30.0), category: "Metabolic" },
BiomarkerReference { name: "Vitamin D", unit: "ng/mL", normal_low: 30.0, normal_high: 80.0, critical_low: Some(10.0), critical_high: Some(150.0), category: "Nutritional" },
BiomarkerReference { name: "CRP", unit: "mg/L", normal_low: 0.0, normal_high: 3.0, critical_low: None, critical_high: Some(10.0), category: "Inflammatory" },
BiomarkerReference { name: "TSH", unit: "mIU/L", normal_low: 0.4, normal_high: 4.0, critical_low: Some(0.1), critical_high: Some(10.0), category: "Thyroid" },
BiomarkerReference { name: "Ferritin", unit: "ng/mL", normal_low: 20.0, normal_high: 250.0, critical_low: Some(10.0), critical_high: Some(1000.0), category: "Iron" },
BiomarkerReference { name: "Vitamin B12", unit: "pg/mL", normal_low: 200.0, normal_high: 900.0, critical_low: Some(150.0), critical_high: None, category: "Nutritional" },
BiomarkerReference { name: "Lp(a)", unit: "nmol/L", normal_low: 0.0, normal_high: 75.0, critical_low: None, critical_high: Some(200.0), category: "Lipid" },
];
pub fn biomarker_references() -> &'static [BiomarkerReference] { REFERENCES }
pub fn z_score(value: f64, reference: &BiomarkerReference) -> f64 {
let mid = (reference.normal_low + reference.normal_high) / 2.0;
let half_range = (reference.normal_high - reference.normal_low) / 2.0;
if half_range == 0.0 {
return 0.0;
}
(value - mid) / half_range
}
pub fn classify_biomarker(value: f64, reference: &BiomarkerReference) -> BiomarkerClassification {
if let Some(cl) = reference.critical_low {
if value < cl {
return BiomarkerClassification::CriticalLow;
}
}
if value < reference.normal_low {
return BiomarkerClassification::Low;
}
if let Some(ch) = reference.critical_high {
if value > ch {
return BiomarkerClassification::CriticalHigh;
}
}
if value > reference.normal_high {
return BiomarkerClassification::High;
}
BiomarkerClassification::Normal
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryScore {
pub category: String,
pub score: f64,
pub confidence: f64,
pub contributing_variants: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BiomarkerProfile {
pub subject_id: String,
pub timestamp: i64,
pub category_scores: HashMap<String, CategoryScore>,
pub global_risk_score: f64,
pub profile_vector: Vec<f32>,
pub biomarker_values: HashMap<String, f64>,
}
struct SnpDef {
rsid: &'static str,
category: &'static str,
w_ref: f64,
w_het: f64,
w_alt: f64,
hom_ref: &'static str,
het: &'static str,
hom_alt: &'static str,
maf: f64, }
static SNPS: &[SnpDef] = &[
SnpDef { rsid: "rs429358", category: "Neurological", w_ref: 0.0, w_het: 0.4, w_alt: 0.9, hom_ref: "TT", het: "CT", hom_alt: "CC", maf: 0.14 },
SnpDef { rsid: "rs7412", category: "Neurological", w_ref: 0.0, w_het: -0.15, w_alt: -0.3, hom_ref: "CC", het: "CT", hom_alt: "TT", maf: 0.08 },
SnpDef { rsid: "rs1042522", category: "Cancer Risk", w_ref: 0.0, w_het: 0.25, w_alt: 0.5, hom_ref: "CC", het: "CG", hom_alt: "GG", maf: 0.40 },
SnpDef { rsid: "rs80357906", category: "Cancer Risk", w_ref: 0.0, w_het: 0.7, w_alt: 0.95, hom_ref: "DD", het: "DI", hom_alt: "II", maf: 0.003 },
SnpDef { rsid: "rs28897696", category: "Cancer Risk", w_ref: 0.0, w_het: 0.3, w_alt: 0.6, hom_ref: "GG", het: "AG", hom_alt: "AA", maf: 0.005 },
SnpDef { rsid: "rs11571833", category: "Cancer Risk", w_ref: 0.0, w_het: 0.20, w_alt: 0.5, hom_ref: "AA", het: "AT", hom_alt: "TT", maf: 0.01 },
SnpDef { rsid: "rs1801133", category: "Metabolism", w_ref: 0.0, w_het: 0.35, w_alt: 0.7, hom_ref: "GG", het: "AG", hom_alt: "AA", maf: 0.32 },
SnpDef { rsid: "rs1801131", category: "Metabolism", w_ref: 0.0, w_het: 0.10, w_alt: 0.25, hom_ref: "TT", het: "GT", hom_alt: "GG", maf: 0.30 },
SnpDef { rsid: "rs4680", category: "Neurological", w_ref: 0.0, w_het: 0.2, w_alt: 0.45, hom_ref: "GG", het: "AG", hom_alt: "AA", maf: 0.50 },
SnpDef { rsid: "rs1799971", category: "Neurological", w_ref: 0.0, w_het: 0.2, w_alt: 0.4, hom_ref: "AA", het: "AG", hom_alt: "GG", maf: 0.15 },
SnpDef { rsid: "rs762551", category: "Metabolism", w_ref: 0.0, w_het: 0.15, w_alt: 0.35, hom_ref: "AA", het: "AC", hom_alt: "CC", maf: 0.37 },
SnpDef { rsid: "rs4988235", category: "Metabolism", w_ref: 0.0, w_het: 0.05, w_alt: 0.15, hom_ref: "AA", het: "AG", hom_alt: "GG", maf: 0.24 },
SnpDef { rsid: "rs53576", category: "Neurological", w_ref: 0.0, w_het: 0.1, w_alt: 0.25, hom_ref: "GG", het: "AG", hom_alt: "AA", maf: 0.35 },
SnpDef { rsid: "rs6311", category: "Neurological", w_ref: 0.0, w_het: 0.15, w_alt: 0.3, hom_ref: "CC", het: "CT", hom_alt: "TT", maf: 0.45 },
SnpDef { rsid: "rs1800497", category: "Neurological", w_ref: 0.0, w_het: 0.25, w_alt: 0.5, hom_ref: "GG", het: "AG", hom_alt: "AA", maf: 0.20 },
SnpDef { rsid: "rs4363657", category: "Cardiovascular", w_ref: 0.0, w_het: 0.35, w_alt: 0.7, hom_ref: "TT", het: "CT", hom_alt: "CC", maf: 0.15 },
SnpDef { rsid: "rs1800566", category: "Cancer Risk", w_ref: 0.0, w_het: 0.15, w_alt: 0.30, hom_ref: "CC", het: "CT", hom_alt: "TT", maf: 0.22 },
SnpDef { rsid: "rs10455872", category: "Cardiovascular", w_ref: 0.0, w_het: 0.40, w_alt: 0.75, hom_ref: "AA", het: "AG", hom_alt: "GG", maf: 0.07 },
SnpDef { rsid: "rs3798220", category: "Cardiovascular", w_ref: 0.0, w_het: 0.35, w_alt: 0.65, hom_ref: "TT", het: "CT", hom_alt: "CC", maf: 0.02 },
SnpDef { rsid: "rs11591147", category: "Cardiovascular", w_ref: 0.0, w_het: -0.30, w_alt: -0.55, hom_ref: "GG", het: "GT", hom_alt: "TT", maf: 0.024 },
];
const NUM_ONEHOT_SNPS: usize = 17;
const NUM_SNPS: usize = 20;
fn genotype_code(snp: &SnpDef, gt: &str) -> u8 {
if gt == snp.hom_ref { 0 }
else if gt.len() == 2 && gt.as_bytes()[0] != gt.as_bytes()[1] { 1 }
else { 2 }
}
fn snp_weight(snp: &SnpDef, code: u8) -> f64 {
match code { 0 => snp.w_ref, 1 => snp.w_het, _ => snp.w_alt }
}
struct Interaction {
rsid_a: &'static str,
rsid_b: &'static str,
modifier: f64,
category: &'static str,
}
static INTERACTIONS: &[Interaction] = &[
Interaction { rsid_a: "rs4680", rsid_b: "rs1799971", modifier: 1.4, category: "Neurological" },
Interaction { rsid_a: "rs1801133", rsid_b: "rs1801131", modifier: 1.3, category: "Metabolism" },
Interaction { rsid_a: "rs429358", rsid_b: "rs1042522", modifier: 1.2, category: "Cancer Risk" },
Interaction { rsid_a: "rs80357906",rsid_b: "rs1042522", modifier: 1.5, category: "Cancer Risk" },
Interaction { rsid_a: "rs1801131", rsid_b: "rs4680", modifier: 1.25, category: "Neurological" }, Interaction { rsid_a: "rs1800497", rsid_b: "rs4680", modifier: 1.2, category: "Neurological" }, ];
fn snp_idx(rsid: &str) -> Option<usize> {
SNPS.iter().position(|s| s.rsid == rsid)
}
fn is_non_ref(gts: &HashMap<String, String>, rsid: &str) -> bool {
match (gts.get(rsid), snp_idx(rsid)) {
(Some(g), Some(idx)) => g != SNPS[idx].hom_ref,
_ => false,
}
}
fn interaction_mod(gts: &HashMap<String, String>, ix: &Interaction) -> f64 {
if is_non_ref(gts, ix.rsid_a) && is_non_ref(gts, ix.rsid_b) {
ix.modifier
} else {
1.0
}
}
struct CategoryMeta { name: &'static str, max_possible: f64, expected_count: usize }
static CAT_ORDER: &[&str] = &["Cancer Risk", "Cardiovascular", "Neurological", "Metabolism"];
fn category_meta() -> &'static [CategoryMeta] {
use std::sync::LazyLock;
static META: LazyLock<Vec<CategoryMeta>> = LazyLock::new(|| {
CAT_ORDER.iter().map(|&cat| {
let (mp, ec) = SNPS.iter().filter(|s| s.category == cat)
.fold((0.0, 0usize), |(s, n), snp| (s + snp.w_alt.max(0.0), n + 1));
CategoryMeta { name: cat, max_possible: mp.max(1.0), expected_count: ec }
}).collect()
});
&META
}
pub fn compute_risk_scores(genotypes: &HashMap<String, String>) -> BiomarkerProfile {
let meta = category_meta();
let mut cat_scores: HashMap<&str, (f64, Vec<String>, usize)> = HashMap::with_capacity(4);
for snp in SNPS {
if let Some(gt) = genotypes.get(snp.rsid) {
let code = genotype_code(snp, gt);
let w = snp_weight(snp, code);
let entry = cat_scores.entry(snp.category).or_insert_with(|| (0.0, Vec::new(), 0));
entry.0 += w;
entry.2 += 1;
if code > 0 {
entry.1.push(snp.rsid.to_string());
}
}
}
for inter in INTERACTIONS {
let m = interaction_mod(genotypes, inter);
if m > 1.0 {
if let Some(entry) = cat_scores.get_mut(inter.category) {
entry.0 *= m;
}
}
}
let mut category_scores = HashMap::with_capacity(meta.len());
for cm in meta {
let (raw, variants, count) = cat_scores.remove(cm.name).unwrap_or((0.0, Vec::new(), 0));
let score = (raw / cm.max_possible).clamp(0.0, 1.0);
let confidence = if count > 0 { (count as f64 / cm.expected_count.max(1) as f64).min(1.0) } else { 0.0 };
let cat = cm.name.to_string();
category_scores.insert(cat.clone(), CategoryScore { category: cat, score, confidence, contributing_variants: variants });
}
let (ws, cs) = category_scores.values()
.fold((0.0, 0.0), |(ws, cs), c| (ws + c.score * c.confidence, cs + c.confidence));
let global = if cs > 0.0 { ws / cs } else { 0.0 };
let mut profile = BiomarkerProfile {
subject_id: String::new(), timestamp: 0, category_scores,
global_risk_score: global, profile_vector: Vec::new(), biomarker_values: HashMap::new(),
};
profile.profile_vector = encode_profile_vector_with_genotypes(&profile, genotypes);
profile
}
pub fn encode_profile_vector(profile: &BiomarkerProfile) -> Vec<f32> {
encode_profile_vector_with_genotypes(profile, &HashMap::new())
}
fn encode_profile_vector_with_genotypes(profile: &BiomarkerProfile, genotypes: &HashMap<String, String>) -> Vec<f32> {
let mut v = vec![0.0f32; 64];
for (i, snp) in SNPS.iter().take(NUM_ONEHOT_SNPS).enumerate() {
let code = genotypes.get(snp.rsid).map(|gt| genotype_code(snp, gt)).unwrap_or(0);
v[i * 3 + code as usize] = 1.0;
}
for (j, cat) in CAT_ORDER.iter().enumerate() {
v[51 + j] = profile.category_scores.get(*cat).map(|c| c.score as f32).unwrap_or(0.0);
}
v[55] = profile.global_risk_score as f32;
for (j, inter) in INTERACTIONS.iter().take(4).enumerate() {
let m = interaction_mod(genotypes, inter);
v[56 + j] = if m > 1.0 { (m - 1.0) as f32 } else { 0.0 };
}
v[60] = analyze_mthfr(genotypes).score as f32 / 4.0;
v[61] = analyze_pain(genotypes).map(|p| p.score as f32 / 4.0).unwrap_or(0.0);
v[62] = genotypes.get("rs429358").map(|g| genotype_code(&SNPS[0], g) as f32 / 2.0).unwrap_or(0.0);
let lpa = SNPS.iter().filter(|s| s.rsid == "rs10455872" || s.rsid == "rs3798220")
.filter_map(|s| genotypes.get(s.rsid).map(|g| genotype_code(s, g) as f32 / 2.0))
.sum::<f32>() / 2.0;
v[63] = lpa;
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 { v.iter_mut().for_each(|x| *x /= norm); }
v
}
fn random_genotype(rng: &mut StdRng, snp: &SnpDef) -> String {
let p = snp.maf;
let q = 1.0 - p;
let r: f64 = rng.gen();
if r < q * q { snp.hom_ref } else if r < q * q + 2.0 * p * q { snp.het } else { snp.hom_alt }.to_string()
}
pub fn generate_synthetic_population(count: usize, seed: u64) -> Vec<BiomarkerProfile> {
let mut rng = StdRng::seed_from_u64(seed);
let mut pop = Vec::with_capacity(count);
for i in 0..count {
let mut genotypes = HashMap::with_capacity(NUM_SNPS);
for snp in SNPS {
genotypes.insert(snp.rsid.to_string(), random_genotype(&mut rng, snp));
}
let mut profile = compute_risk_scores(&genotypes);
profile.subject_id = format!("SYN-{:06}", i);
profile.timestamp = 1700000000 + i as i64;
let mthfr_score = analyze_mthfr(&genotypes).score;
let apoe_code = genotypes.get("rs429358").map(|g| genotype_code(&SNPS[0], g)).unwrap_or(0);
let nqo1_idx = SNPS.iter().position(|s| s.rsid == "rs1800566").unwrap();
let nqo1_code = genotypes.get("rs1800566").map(|g| genotype_code(&SNPS[nqo1_idx], g)).unwrap_or(0);
let lpa_risk: u8 = SNPS.iter().filter(|s| s.rsid == "rs10455872" || s.rsid == "rs3798220")
.filter_map(|s| genotypes.get(s.rsid).map(|g| genotype_code(s, g)))
.sum();
let pcsk9_idx = SNPS.iter().position(|s| s.rsid == "rs11591147").unwrap();
let pcsk9_code = genotypes.get("rs11591147").map(|g| genotype_code(&SNPS[pcsk9_idx], g)).unwrap_or(0);
profile.biomarker_values.reserve(REFERENCES.len());
for bref in REFERENCES {
let mid = (bref.normal_low + bref.normal_high) / 2.0;
let sd = (bref.normal_high - bref.normal_low) / 4.0;
let mut val = mid + rng.gen_range(-1.5..1.5) * sd;
let nm = bref.name;
if nm == "Homocysteine" && mthfr_score >= 2 { val += sd * (mthfr_score as f64 - 1.0); }
if (nm == "Total Cholesterol" || nm == "LDL") && apoe_code > 0 { val += sd * 0.5 * apoe_code as f64; }
if nm == "HDL" && apoe_code > 0 { val -= sd * 0.3 * apoe_code as f64; }
if nm == "Triglycerides" && apoe_code > 0 { val += sd * 0.4 * apoe_code as f64; }
if nm == "Vitamin B12" && mthfr_score >= 2 { val -= sd * 0.4; }
if nm == "CRP" && nqo1_code == 2 { val += sd * 0.3; }
if nm == "Lp(a)" && lpa_risk > 0 { val += sd * 1.5 * lpa_risk as f64; }
if (nm == "LDL" || nm == "Total Cholesterol") && pcsk9_code > 0 { val -= sd * 0.6 * pcsk9_code as f64; }
val = val.max(bref.critical_low.unwrap_or(0.0)).max(0.0);
if let Some(ch) = bref.critical_high { val = val.min(ch * 1.2); }
profile.biomarker_values.insert(bref.name.to_string(), (val * 10.0).round() / 10.0);
}
pop.push(profile);
}
pop
}
#[cfg(test)]
mod tests {
use super::*;
fn full_hom_ref() -> HashMap<String, String> {
SNPS.iter().map(|s| (s.rsid.to_string(), s.hom_ref.to_string())).collect()
}
#[test]
fn test_z_score_midpoint_is_zero() {
let r = &REFERENCES[0]; let mid = (r.normal_low + r.normal_high) / 2.0;
assert!((z_score(mid, r)).abs() < 1e-10);
}
#[test]
fn test_z_score_high_bound_is_one() {
let r = &REFERENCES[0];
assert!((z_score(r.normal_high, r) - 1.0).abs() < 1e-10);
}
#[test]
fn test_classify_normal() {
let r = &REFERENCES[0]; assert_eq!(classify_biomarker(150.0, r), BiomarkerClassification::Normal);
}
#[test]
fn test_classify_critical_high() {
let r = &REFERENCES[0]; assert_eq!(classify_biomarker(350.0, r), BiomarkerClassification::CriticalHigh);
}
#[test]
fn test_classify_low() {
let r = &REFERENCES[0]; assert_eq!(classify_biomarker(110.0, r), BiomarkerClassification::Low);
}
#[test]
fn test_classify_critical_low() {
let r = &REFERENCES[0]; assert_eq!(classify_biomarker(90.0, r), BiomarkerClassification::CriticalLow);
}
#[test]
fn test_risk_scores_all_hom_ref_low_risk() {
let gts = full_hom_ref();
let profile = compute_risk_scores(>s);
assert!(profile.global_risk_score < 0.15, "hom-ref should be low risk, got {}", profile.global_risk_score);
}
#[test]
fn test_risk_scores_high_cancer_risk() {
let mut gts = full_hom_ref();
gts.insert("rs80357906".into(), "DI".into());
gts.insert("rs1042522".into(), "GG".into());
gts.insert("rs11571833".into(), "TT".into());
let profile = compute_risk_scores(>s);
let cancer = profile.category_scores.get("Cancer Risk").unwrap();
assert!(cancer.score > 0.3, "should have elevated cancer risk, got {}", cancer.score);
}
#[test]
fn test_vector_dimension_is_64() {
let gts = full_hom_ref();
let profile = compute_risk_scores(>s);
assert_eq!(profile.profile_vector.len(), 64);
}
#[test]
fn test_vector_is_l2_normalized() {
let mut gts = full_hom_ref();
gts.insert("rs4680".into(), "AG".into());
gts.insert("rs1799971".into(), "AG".into());
let profile = compute_risk_scores(>s);
let norm: f32 = profile.profile_vector.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-4, "vector should be L2-normalized, got norm={}", norm);
}
#[test]
fn test_interaction_comt_oprm1() {
let mut gts = full_hom_ref();
gts.insert("rs4680".into(), "AA".into());
gts.insert("rs1799971".into(), "GG".into());
let with_interaction = compute_risk_scores(>s);
let neuro_inter = with_interaction.category_scores.get("Neurological").unwrap().score;
let mut gts2 = full_hom_ref();
gts2.insert("rs4680".into(), "AA".into());
let without_full = compute_risk_scores(>s2);
let neuro_single = without_full.category_scores.get("Neurological").unwrap().score;
assert!(neuro_inter > neuro_single, "interaction should amplify risk");
}
#[test]
fn test_interaction_brca1_tp53() {
let mut gts = full_hom_ref();
gts.insert("rs80357906".into(), "DI".into());
gts.insert("rs1042522".into(), "GG".into());
let profile = compute_risk_scores(>s);
let cancer = profile.category_scores.get("Cancer Risk").unwrap();
assert!(cancer.contributing_variants.contains(&"rs80357906".to_string()));
assert!(cancer.contributing_variants.contains(&"rs1042522".to_string()));
}
#[test]
fn test_population_generation() {
let pop = generate_synthetic_population(50, 42);
assert_eq!(pop.len(), 50);
for p in &pop {
assert_eq!(p.profile_vector.len(), 64);
assert!(!p.biomarker_values.is_empty());
assert!(p.global_risk_score >= 0.0 && p.global_risk_score <= 1.0);
}
}
#[test]
fn test_population_deterministic() {
let a = generate_synthetic_population(10, 99);
let b = generate_synthetic_population(10, 99);
for (pa, pb) in a.iter().zip(b.iter()) {
assert_eq!(pa.subject_id, pb.subject_id);
assert!((pa.global_risk_score - pb.global_risk_score).abs() < 1e-10);
}
}
#[test]
fn test_mthfr_elevates_homocysteine() {
let pop = generate_synthetic_population(200, 7);
let (mut mthfr_high, mut mthfr_low) = (Vec::new(), Vec::new());
for p in &pop {
let hcy = p.biomarker_values.get("Homocysteine").copied().unwrap_or(0.0);
let mthfr_score = p.category_scores.get("Metabolism").map(|c| c.score).unwrap_or(0.0);
if mthfr_score > 0.3 { mthfr_high.push(hcy); } else { mthfr_low.push(hcy); }
}
if !mthfr_high.is_empty() && !mthfr_low.is_empty() {
let avg_high: f64 = mthfr_high.iter().sum::<f64>() / mthfr_high.len() as f64;
let avg_low: f64 = mthfr_low.iter().sum::<f64>() / mthfr_low.len() as f64;
assert!(avg_high > avg_low, "MTHFR variants should elevate homocysteine: high={}, low={}", avg_high, avg_low);
}
}
#[test]
fn test_biomarker_references_count() {
assert_eq!(biomarker_references().len(), 13);
}
}