use crate::params::ParamState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum LifeStage {
Infant, Child, Adolescent, YoungAdult, MiddleAge, Senior, Elderly, }
impl LifeStage {
pub fn from_age_years(age_years: f32) -> Self {
if age_years < 3.0 {
LifeStage::Infant
} else if age_years < 13.0 {
LifeStage::Child
} else if age_years < 18.0 {
LifeStage::Adolescent
} else if age_years < 36.0 {
LifeStage::YoungAdult
} else if age_years < 56.0 {
LifeStage::MiddleAge
} else if age_years < 76.0 {
LifeStage::Senior
} else {
LifeStage::Elderly
}
}
pub fn midpoint_years(&self) -> f32 {
match self {
LifeStage::Infant => 1.0,
LifeStage::Child => 7.5,
LifeStage::Adolescent => 15.0,
LifeStage::YoungAdult => 26.5,
LifeStage::MiddleAge => 45.5,
LifeStage::Senior => 65.5,
LifeStage::Elderly => 85.0,
}
}
pub fn label(&self) -> &'static str {
match self {
LifeStage::Infant => "Infant",
LifeStage::Child => "Child",
LifeStage::Adolescent => "Adolescent",
LifeStage::YoungAdult => "Young Adult",
LifeStage::MiddleAge => "Middle Age",
LifeStage::Senior => "Senior",
LifeStage::Elderly => "Elderly",
}
}
pub fn all() -> [LifeStage; 7] {
[
LifeStage::Infant,
LifeStage::Child,
LifeStage::Adolescent,
LifeStage::YoungAdult,
LifeStage::MiddleAge,
LifeStage::Senior,
LifeStage::Elderly,
]
}
}
fn compute_height_factor(age: f32) -> f32 {
let age = age.clamp(0.0, 120.0);
if age <= 18.0 {
let t = age / 18.0; let s = t * t * (3.0 - 2.0 * t);
0.3 + 0.7 * s
} else if age <= 60.0 {
let t = (age - 18.0) / (60.0 - 18.0);
1.0 - 0.03 * t
} else {
let t = ((age - 60.0) / (90.0 - 60.0)).min(1.0);
0.97 - 0.04 * t
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct AgeProfile {
pub age_years: f32,
pub life_stage: LifeStage,
pub height_factor: f32,
pub weight_tendency: f32,
pub muscle_factor: f32,
pub skin_elasticity: f32,
pub bone_density: f32,
pub posture: f32,
}
impl AgeProfile {
pub fn for_age(age_years: f32) -> Self {
let age = age_years.clamp(0.0, 120.0);
let life_stage = LifeStage::from_age_years(age);
let height_factor = compute_height_factor(age);
let weight_tendency = if age <= 18.0 {
let t = age / 18.0;
0.2 + 0.3 * t
} else if age <= 50.0 {
let t = (age - 18.0) / (50.0 - 18.0);
0.5 + 0.2 * t
} else if age <= 80.0 {
let t = (age - 50.0) / (80.0 - 50.0);
0.7 - 0.1 * t
} else {
let t = ((age - 80.0) / 40.0).min(1.0);
0.6 - 0.15 * t
};
let muscle_factor = if age <= 25.0 {
let t = age / 25.0;
0.3 + 0.7 * t * t * (3.0 - 2.0 * t)
} else if age <= 35.0 {
1.0
} else if age <= 80.0 {
let t = (age - 35.0) / (80.0 - 35.0);
1.0 - 0.6 * t
} else {
let t = ((age - 80.0) / 40.0).min(1.0);
0.4 - 0.2 * t
};
let skin_elasticity = if age <= 20.0 {
1.0
} else {
let t = ((age - 20.0) / 100.0).min(1.0);
1.0 - 0.85 * t
};
let bone_density = if age <= 30.0 {
let t = age / 30.0;
0.5 + 0.5 * t
} else if age <= 90.0 {
let t = (age - 30.0) / (90.0 - 30.0);
1.0 - 0.5 * t
} else {
let t = ((age - 90.0) / 30.0).min(1.0);
0.5 - 0.2 * t
};
let posture = if age <= 40.0 {
1.0
} else if age <= 90.0 {
let t = (age - 40.0) / (90.0 - 40.0);
1.0 - 0.35 * t
} else {
let t = ((age - 90.0) / 30.0).min(1.0);
0.65 - 0.2 * t
};
AgeProfile {
age_years: age,
life_stage,
height_factor: height_factor.clamp(0.1, 1.0),
weight_tendency: weight_tendency.clamp(0.0, 1.0),
muscle_factor: muscle_factor.clamp(0.0, 1.0),
skin_elasticity: skin_elasticity.clamp(0.0, 1.0),
bone_density: bone_density.clamp(0.0, 1.0),
posture: posture.clamp(0.0, 1.0),
}
}
pub fn apply_to_params(&self, base: &ParamState) -> ParamState {
let mut result = base.clone();
result.height = (base.height * self.height_factor).clamp(0.0, 1.0);
result.weight = (base.weight * 0.7 + self.weight_tendency * 0.3).clamp(0.0, 1.0);
result.muscle = (base.muscle * self.muscle_factor).clamp(0.0, 1.0);
result.age = years_to_param_age(self.age_years);
result
.extra
.insert("skin_elasticity".to_string(), self.skin_elasticity);
result
.extra
.insert("bone_density".to_string(), self.bone_density);
result.extra.insert("posture".to_string(), self.posture);
result
}
pub fn lerp(&self, other: &AgeProfile, t: f32) -> AgeProfile {
let t = t.clamp(0.0, 1.0);
let age_years = self.age_years + (other.age_years - self.age_years) * t;
AgeProfile {
age_years,
life_stage: LifeStage::from_age_years(age_years),
height_factor: self.height_factor + (other.height_factor - self.height_factor) * t,
weight_tendency: self.weight_tendency
+ (other.weight_tendency - self.weight_tendency) * t,
muscle_factor: self.muscle_factor + (other.muscle_factor - self.muscle_factor) * t,
skin_elasticity: self.skin_elasticity
+ (other.skin_elasticity - self.skin_elasticity) * t,
bone_density: self.bone_density + (other.bone_density - self.bone_density) * t,
posture: self.posture + (other.posture - self.posture) * t,
}
}
}
pub fn age_progression(age_start: f32, age_end: f32, steps: usize) -> Vec<AgeProfile> {
if steps == 0 {
return Vec::new();
}
if steps == 1 {
return vec![AgeProfile::for_age(age_start)];
}
(0..steps)
.map(|i| {
let t = i as f32 / (steps - 1) as f32;
let age = age_start + (age_end - age_start) * t;
AgeProfile::for_age(age)
})
.collect()
}
pub fn param_age_to_years(param_age: f32) -> f32 {
let t = param_age.clamp(0.0, 1.0);
if t <= 0.5 {
t * 2.0 * 35.0
} else {
35.0 + (t - 0.5) * 2.0 * 55.0
}
}
pub fn years_to_param_age(years: f32) -> f32 {
let y = years.clamp(0.0, 90.0);
if y <= 35.0 {
y / 35.0 * 0.5
} else {
0.5 + (y - 35.0) / 55.0 * 0.5
}
}
pub fn aging_delta(age_a: f32, age_b: f32) -> String {
let profile_a = AgeProfile::for_age(age_a);
let profile_b = AgeProfile::for_age(age_b);
let dh = profile_b.height_factor - profile_a.height_factor;
let dw = profile_b.weight_tendency - profile_a.weight_tendency;
let dm = profile_b.muscle_factor - profile_a.muscle_factor;
let ds = profile_b.skin_elasticity - profile_a.skin_elasticity;
let db = profile_b.bone_density - profile_a.bone_density;
let dp = profile_b.posture - profile_a.posture;
format!(
"Age {:.1}y ({}) -> {:.1}y ({}): \
height_factor {:+.3}, \
weight_tendency {:+.3}, \
muscle {:+.3}, \
skin_elasticity {:+.3}, \
bone_density {:+.3}, \
posture {:+.3}",
age_a,
profile_a.life_stage.label(),
age_b,
profile_b.life_stage.label(),
dh,
dw,
dm,
ds,
db,
dp,
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(dead_code)]
pub enum BmiCategory {
Underweight,
Normal,
Overweight,
Obese,
}
pub fn estimate_bmi_category(weight_param: f32) -> BmiCategory {
let w = weight_param.clamp(0.0, 1.0);
if w < 0.25 {
BmiCategory::Underweight
} else if w < 0.55 {
BmiCategory::Normal
} else if w < 0.75 {
BmiCategory::Overweight
} else {
BmiCategory::Obese
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn life_stage_infant_from_age_1() {
assert_eq!(LifeStage::from_age_years(1.0), LifeStage::Infant);
}
#[test]
fn life_stage_child_from_age_8() {
assert_eq!(LifeStage::from_age_years(8.0), LifeStage::Child);
}
#[test]
fn life_stage_young_adult_from_age_25() {
assert_eq!(LifeStage::from_age_years(25.0), LifeStage::YoungAdult);
}
#[test]
fn life_stage_senior_from_age_65() {
assert_eq!(LifeStage::from_age_years(65.0), LifeStage::Senior);
}
#[test]
fn life_stage_all_has_seven() {
assert_eq!(LifeStage::all().len(), 7);
}
#[test]
fn age_profile_infant_short_height() {
let p = AgeProfile::for_age(1.0);
assert!(
p.height_factor < 0.5,
"infant height_factor={}",
p.height_factor
);
}
#[test]
fn age_profile_adult_full_height() {
let p = AgeProfile::for_age(25.0);
assert!(
p.height_factor > 0.95,
"adult height_factor={}",
p.height_factor
);
}
#[test]
fn age_profile_elderly_reduced_height() {
let p = AgeProfile::for_age(80.0);
assert!(
p.height_factor < 0.97,
"elderly height_factor={}",
p.height_factor
);
}
#[test]
fn age_profile_muscle_peaks_young_adult() {
let young = AgeProfile::for_age(30.0);
let old = AgeProfile::for_age(75.0);
assert!(
young.muscle_factor > old.muscle_factor,
"young={} old={}",
young.muscle_factor,
old.muscle_factor
);
}
#[test]
fn age_profile_lerp_midpoint() {
let a = AgeProfile::for_age(20.0);
let b = AgeProfile::for_age(60.0);
let mid = a.lerp(&b, 0.5);
assert!((mid.age_years - 40.0).abs() < 1e-4);
let hf = mid.height_factor;
let lo = a.height_factor.min(b.height_factor);
let hi = a.height_factor.max(b.height_factor);
assert!(hf >= lo - 1e-5 && hf <= hi + 1e-5);
}
#[test]
fn age_progression_correct_length() {
let profiles = age_progression(0.0, 90.0, 10);
assert_eq!(profiles.len(), 10);
assert!((profiles[0].age_years - 0.0).abs() < 1e-4);
assert!((profiles[9].age_years - 90.0).abs() < 1e-4);
}
#[test]
fn param_age_to_years_zero_is_zero() {
let years = param_age_to_years(0.0);
assert!((years - 0.0).abs() < 1e-5, "years={}", years);
}
#[test]
fn param_age_to_years_half_is_midlife() {
let years = param_age_to_years(0.5);
assert!((years - 35.0).abs() < 1e-5, "years={}", years);
}
#[test]
fn years_to_param_age_roundtrip() {
for y in [0.0f32, 10.0, 25.0, 35.0, 50.0, 70.0, 90.0] {
let p = years_to_param_age(y);
let back = param_age_to_years(p);
assert!((back - y).abs() < 1e-3, "y={} p={} back={}", y, p, back);
}
}
#[test]
fn estimate_bmi_normal_range() {
assert_eq!(estimate_bmi_category(0.4), BmiCategory::Normal);
assert_eq!(estimate_bmi_category(0.1), BmiCategory::Underweight);
assert_eq!(estimate_bmi_category(0.65), BmiCategory::Overweight);
assert_eq!(estimate_bmi_category(0.9), BmiCategory::Obese);
}
#[test]
fn aging_delta_string_not_empty() {
let s = aging_delta(20.0, 70.0);
assert!(!s.is_empty());
assert!(s.contains("->"), "missing arrow in: {}", s);
}
}