use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpGSite {
pub chromosome: u8,
pub position: u64,
pub methylation_level: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethylationProfile {
pub sites: Vec<CpGSite>,
}
impl MethylationProfile {
pub fn from_beta_values(positions: Vec<(u8, u64)>, betas: Vec<f32>) -> Self {
let sites = positions
.into_iter()
.zip(betas.into_iter())
.map(|((chr, pos), beta)| CpGSite {
chromosome: chr,
position: pos,
methylation_level: beta.clamp(0.0, 1.0),
})
.collect();
Self { sites }
}
pub fn mean_methylation(&self) -> f32 {
if self.sites.is_empty() {
return 0.0;
}
let sum: f32 = self.sites.iter().map(|s| s.methylation_level).sum();
sum / self.sites.len() as f32
}
pub fn methylation_entropy(&self) -> f64 {
if self.sites.is_empty() {
return 0.0;
}
let mut bins = [0u32; 10];
for site in &self.sites {
let bin = ((site.methylation_level * 10.0) as usize).min(9);
bins[bin] += 1;
}
let n = self.sites.len() as f64;
let mut entropy = 0.0;
for &count in &bins {
if count > 0 {
let p = count as f64 / n;
entropy -= p * p.ln();
}
}
entropy
}
pub fn extreme_methylation_ratio(&self) -> f32 {
if self.sites.is_empty() {
return 0.0;
}
let extreme_count = self
.sites
.iter()
.filter(|s| s.methylation_level < 0.1 || s.methylation_level > 0.9)
.count();
extreme_count as f32 / self.sites.len() as f32
}
}
pub struct HorvathClock {
intercept: f64,
coefficients: Vec<f64>,
num_bins: usize,
}
impl HorvathClock {
pub fn default_clock() -> Self {
Self {
intercept: 30.0,
coefficients: vec![
-15.0, 10.0, 0.5, ],
num_bins: 3,
}
}
pub fn predict_age(&self, profile: &MethylationProfile) -> f64 {
if profile.sites.is_empty() {
return self.intercept;
}
let bin_size = profile.sites.len() / self.num_bins.max(1);
let mut age = self.intercept;
for (bin_idx, coefficient) in self.coefficients.iter().enumerate() {
let start = bin_idx * bin_size;
let end = ((bin_idx + 1) * bin_size).min(profile.sites.len());
if start >= profile.sites.len() {
break;
}
let bin_sites = &profile.sites[start..end];
if !bin_sites.is_empty() {
let mean_meth: f64 = bin_sites
.iter()
.map(|s| s.methylation_level as f64)
.sum::<f64>()
/ bin_sites.len() as f64;
age += coefficient * mean_meth;
}
}
age.max(0.0)
}
pub fn age_acceleration(predicted_age: f64, chronological_age: f64) -> f64 {
predicted_age - chronological_age
}
}
pub struct CancerSignalDetector {
entropy_weight: f64,
extreme_weight: f64,
risk_threshold: f64,
}
impl CancerSignalDetector {
pub fn new() -> Self {
Self {
entropy_weight: 0.4,
extreme_weight: 0.6,
risk_threshold: 0.3,
}
}
pub fn detect(&self, profile: &MethylationProfile) -> CancerSignalResult {
if profile.sites.is_empty() {
return CancerSignalResult {
risk_score: 0.0,
is_elevated: false,
entropy: 0.0,
extreme_ratio: 0.0,
};
}
let entropy = profile.methylation_entropy();
let extreme_ratio = profile.extreme_methylation_ratio() as f64;
let normalized_entropy = (entropy / 2.302).min(1.0);
let risk_score = (self.entropy_weight * normalized_entropy
+ self.extreme_weight * extreme_ratio)
.min(1.0);
CancerSignalResult {
risk_score,
is_elevated: risk_score >= self.risk_threshold,
entropy,
extreme_ratio,
}
}
}
impl Default for CancerSignalDetector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancerSignalResult {
pub risk_score: f64,
pub is_elevated: bool,
pub entropy: f64,
pub extreme_ratio: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_methylation_profile() {
let positions = vec![(1, 1000), (1, 2000)];
let betas = vec![0.3, 0.7];
let profile = MethylationProfile::from_beta_values(positions, betas);
assert_eq!(profile.sites.len(), 2);
assert!((profile.mean_methylation() - 0.5).abs() < 0.001);
}
#[test]
fn test_horvath_clock() {
let clock = HorvathClock::default_clock();
let positions = vec![(1, 1000), (1, 2000), (1, 3000)];
let betas = vec![0.5, 0.5, 0.5];
let profile = MethylationProfile::from_beta_values(positions, betas);
let age = clock.predict_age(&profile);
assert!(age > 0.0);
}
#[test]
fn test_age_acceleration() {
let accel = HorvathClock::age_acceleration(55.0, 50.0);
assert!((accel - 5.0).abs() < 0.001);
let decel = HorvathClock::age_acceleration(40.0, 50.0);
assert!((decel - (-10.0)).abs() < 0.001);
}
#[test]
fn test_methylation_entropy() {
let positions: Vec<(u8, u64)> = (0..100).map(|i| (1u8, i as u64)).collect();
let betas = vec![0.5; 100];
let profile = MethylationProfile::from_beta_values(positions, betas);
let entropy = profile.methylation_entropy();
assert!(
entropy < 0.1,
"Uniform should have low entropy: {}",
entropy
);
let positions2: Vec<(u8, u64)> = (0..100).map(|i| (1u8, i as u64)).collect();
let betas2: Vec<f32> = (0..100).map(|i| i as f32 / 100.0).collect();
let profile2 = MethylationProfile::from_beta_values(positions2, betas2);
let entropy2 = profile2.methylation_entropy();
assert!(
entropy2 > 1.0,
"Spread should have high entropy: {}",
entropy2
);
}
#[test]
fn test_cancer_signal_detector() {
let detector = CancerSignalDetector::new();
let positions: Vec<(u8, u64)> = (0..100).map(|i| (1u8, i as u64)).collect();
let betas = vec![0.5; 100];
let profile = MethylationProfile::from_beta_values(positions, betas);
let result = detector.detect(&profile);
assert!(!result.is_elevated, "Normal profile should not be elevated");
assert!(result.risk_score < 0.3);
let positions2: Vec<(u8, u64)> = (0..100).map(|i| (1u8, i as u64)).collect();
let betas2: Vec<f32> = (0..100)
.map(|i| if i % 2 == 0 { 0.95 } else { 0.05 })
.collect();
let profile2 = MethylationProfile::from_beta_values(positions2, betas2);
let result2 = detector.detect(&profile2);
assert!(result2.is_elevated, "Cancer profile should be elevated");
assert!(result2.extreme_ratio > 0.8);
}
}