1#[allow(dead_code)]
7#[derive(Debug, Clone, PartialEq)]
9pub struct AgeStage {
10 pub years: f32,
11 pub fat_pct: f32,
12 pub muscle_pct: f32,
13 pub bone_density: f32,
14 pub skin_elasticity: f32,
15}
16
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
20pub struct AgingCurve {
21 pub stages: Vec<AgeStage>,
22}
23
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
27pub struct AgeProfile {
28 pub base_age: f32,
29 pub sex: u8,
31 pub ethnicity: u8,
33}
34
35pub fn interpolate_age_stages(a: &AgeStage, b: &AgeStage, t: f32) -> AgeStage {
41 let t = t.clamp(0.0, 1.0);
42 AgeStage {
43 years: a.years + (b.years - a.years) * t,
44 fat_pct: a.fat_pct + (b.fat_pct - a.fat_pct) * t,
45 muscle_pct: a.muscle_pct + (b.muscle_pct - a.muscle_pct) * t,
46 bone_density: a.bone_density + (b.bone_density - a.bone_density) * t,
47 skin_elasticity: a.skin_elasticity + (b.skin_elasticity - a.skin_elasticity) * t,
48 }
49}
50
51pub fn compute_age_stage(curve: &AgingCurve, years: f32) -> AgeStage {
53 let stages = &curve.stages;
54 if stages.is_empty() {
55 return AgeStage {
56 years,
57 fat_pct: 0.25,
58 muscle_pct: 0.40,
59 bone_density: 1.0,
60 skin_elasticity: 0.8,
61 };
62 }
63 if years <= stages[0].years {
64 return stages[0].clone();
65 }
66 let last = &stages[stages.len() - 1];
67 if years >= last.years {
68 return last.clone();
69 }
70 for i in 0..stages.len() - 1 {
71 let a = &stages[i];
72 let b = &stages[i + 1];
73 if years >= a.years && years <= b.years {
74 let t = (years - a.years) / (b.years - a.years);
75 return interpolate_age_stages(a, b, t);
76 }
77 }
78 stages[0].clone()
79}
80
81pub fn default_aging_curve_male() -> AgingCurve {
87 AgingCurve {
88 stages: vec![
89 AgeStage {
90 years: 10.0,
91 fat_pct: 0.15,
92 muscle_pct: 0.35,
93 bone_density: 0.70,
94 skin_elasticity: 0.98,
95 },
96 AgeStage {
97 years: 20.0,
98 fat_pct: 0.18,
99 muscle_pct: 0.45,
100 bone_density: 0.95,
101 skin_elasticity: 0.95,
102 },
103 AgeStage {
104 years: 30.0,
105 fat_pct: 0.22,
106 muscle_pct: 0.44,
107 bone_density: 1.00,
108 skin_elasticity: 0.90,
109 },
110 AgeStage {
111 years: 40.0,
112 fat_pct: 0.26,
113 muscle_pct: 0.42,
114 bone_density: 0.98,
115 skin_elasticity: 0.82,
116 },
117 AgeStage {
118 years: 50.0,
119 fat_pct: 0.30,
120 muscle_pct: 0.38,
121 bone_density: 0.93,
122 skin_elasticity: 0.72,
123 },
124 AgeStage {
125 years: 60.0,
126 fat_pct: 0.32,
127 muscle_pct: 0.33,
128 bone_density: 0.86,
129 skin_elasticity: 0.60,
130 },
131 AgeStage {
132 years: 70.0,
133 fat_pct: 0.33,
134 muscle_pct: 0.27,
135 bone_density: 0.76,
136 skin_elasticity: 0.48,
137 },
138 AgeStage {
139 years: 75.0,
140 fat_pct: 0.32,
141 muscle_pct: 0.23,
142 bone_density: 0.68,
143 skin_elasticity: 0.40,
144 },
145 AgeStage {
146 years: 80.0,
147 fat_pct: 0.30,
148 muscle_pct: 0.19,
149 bone_density: 0.60,
150 skin_elasticity: 0.32,
151 },
152 AgeStage {
153 years: 90.0,
154 fat_pct: 0.27,
155 muscle_pct: 0.14,
156 bone_density: 0.50,
157 skin_elasticity: 0.22,
158 },
159 ],
160 }
161}
162
163pub fn default_aging_curve_female() -> AgingCurve {
165 AgingCurve {
166 stages: vec![
167 AgeStage {
168 years: 10.0,
169 fat_pct: 0.20,
170 muscle_pct: 0.30,
171 bone_density: 0.68,
172 skin_elasticity: 0.98,
173 },
174 AgeStage {
175 years: 20.0,
176 fat_pct: 0.24,
177 muscle_pct: 0.36,
178 bone_density: 0.92,
179 skin_elasticity: 0.96,
180 },
181 AgeStage {
182 years: 30.0,
183 fat_pct: 0.27,
184 muscle_pct: 0.35,
185 bone_density: 0.97,
186 skin_elasticity: 0.91,
187 },
188 AgeStage {
189 years: 40.0,
190 fat_pct: 0.30,
191 muscle_pct: 0.33,
192 bone_density: 0.94,
193 skin_elasticity: 0.82,
194 },
195 AgeStage {
196 years: 50.0,
197 fat_pct: 0.34,
198 muscle_pct: 0.29,
199 bone_density: 0.84,
200 skin_elasticity: 0.68,
201 },
202 AgeStage {
203 years: 60.0,
204 fat_pct: 0.36,
205 muscle_pct: 0.24,
206 bone_density: 0.74,
207 skin_elasticity: 0.54,
208 },
209 AgeStage {
210 years: 70.0,
211 fat_pct: 0.35,
212 muscle_pct: 0.19,
213 bone_density: 0.64,
214 skin_elasticity: 0.42,
215 },
216 AgeStage {
217 years: 75.0,
218 fat_pct: 0.33,
219 muscle_pct: 0.16,
220 bone_density: 0.57,
221 skin_elasticity: 0.35,
222 },
223 AgeStage {
224 years: 80.0,
225 fat_pct: 0.30,
226 muscle_pct: 0.13,
227 bone_density: 0.49,
228 skin_elasticity: 0.27,
229 },
230 AgeStage {
231 years: 90.0,
232 fat_pct: 0.26,
233 muscle_pct: 0.09,
234 bone_density: 0.38,
235 skin_elasticity: 0.18,
236 },
237 ],
238 }
239}
240
241pub fn age_progression_deltas(from: &AgeStage, to: &AgeStage) -> Vec<(String, f32)> {
247 vec![
248 ("delta_fat_pct".into(), to.fat_pct - from.fat_pct),
249 ("delta_muscle_pct".into(), to.muscle_pct - from.muscle_pct),
250 (
251 "delta_bone_density".into(),
252 to.bone_density - from.bone_density,
253 ),
254 (
255 "delta_skin_elasticity".into(),
256 to.skin_elasticity - from.skin_elasticity,
257 ),
258 ]
259}
260
261pub fn skin_aging_params(years: f32, sex: u8) -> Vec<(String, f32)> {
263 let age_factor = ((years - 20.0) / 70.0).clamp(0.0, 1.0);
264 let sex_mul = if sex == 1 { 1.05f32 } else { 1.0f32 }; vec![
266 ("wrinkle_forehead".into(), age_factor * 0.8 * sex_mul),
267 ("wrinkle_eyes".into(), age_factor * 0.9 * sex_mul),
268 ("wrinkle_mouth".into(), age_factor * 0.7 * sex_mul),
269 ("sag_cheeks".into(), age_factor * 0.6),
270 ("sag_jowl".into(), age_factor * 0.5),
271 ("sag_neck".into(), age_factor * 0.55),
272 ("age_spots".into(), (age_factor - 0.3).max(0.0) * 0.6),
273 ("pore_size".into(), age_factor * 0.4),
274 ]
275}
276
277pub fn face_aging_params(years: f32) -> Vec<(String, f32)> {
279 let af = ((years - 20.0) / 70.0).clamp(0.0, 1.0);
280 vec![
281 ("brow_droop".into(), af * 0.5),
282 ("jaw_resorption".into(), af * 0.35),
283 ("nose_tip_droop".into(), af * 0.3),
284 ("ear_growth".into(), af * 0.2),
285 ("lip_thinning".into(), af * 0.4),
286 ("nasolabial_depth".into(), af * 0.6),
287 ("eye_hollow".into(), af * 0.45),
288 ]
289}
290
291pub fn body_aging_params(years: f32, sex: u8) -> Vec<(String, f32)> {
293 let af = ((years - 20.0) / 70.0).clamp(0.0, 1.0);
294 let belly_mul = if sex == 0 { 1.2f32 } else { 1.0f32 }; vec![
296 ("posture_kyphosis".into(), af * 0.4),
297 ("belly_fat".into(), af * 0.6 * belly_mul),
298 ("muscle_loss_arms".into(), af * 0.5),
299 ("muscle_loss_legs".into(), af * 0.45),
300 ("height_loss".into(), af * 0.03), ("hip_fat".into(), af * 0.35),
302 ("skin_looseness_body".into(), af * 0.5),
303 ]
304}
305
306pub fn simulate_aging(
308 profile: &AgeProfile,
309 target_years: f32,
310 curve: &AgingCurve,
311) -> Vec<(String, f32)> {
312 let from = compute_age_stage(curve, profile.base_age);
313 let to = compute_age_stage(curve, target_years);
314 let mut params = age_progression_deltas(&from, &to);
315 params.extend(skin_aging_params(target_years, profile.sex));
316 params.extend(face_aging_params(target_years));
317 params.extend(body_aging_params(target_years, profile.sex));
318 params
319}
320
321pub fn reverse_aging(params: &[(String, f32)]) -> Vec<(String, f32)> {
323 params.iter().map(|(k, v)| (k.clone(), -v)).collect()
324}
325
326#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_interpolate_midpoint() {
336 let a = AgeStage {
337 years: 20.0,
338 fat_pct: 0.20,
339 muscle_pct: 0.40,
340 bone_density: 1.0,
341 skin_elasticity: 0.95,
342 };
343 let b = AgeStage {
344 years: 40.0,
345 fat_pct: 0.30,
346 muscle_pct: 0.35,
347 bone_density: 0.90,
348 skin_elasticity: 0.75,
349 };
350 let mid = interpolate_age_stages(&a, &b, 0.5);
351 assert!((mid.fat_pct - 0.25).abs() < 1e-5);
352 assert!((mid.muscle_pct - 0.375).abs() < 1e-5);
353 }
354
355 #[test]
356 fn test_interpolate_clamp_t() {
357 let a = AgeStage {
358 years: 0.0,
359 fat_pct: 0.1,
360 muscle_pct: 0.5,
361 bone_density: 1.0,
362 skin_elasticity: 1.0,
363 };
364 let b = AgeStage {
365 years: 100.0,
366 fat_pct: 0.4,
367 muscle_pct: 0.1,
368 bone_density: 0.4,
369 skin_elasticity: 0.2,
370 };
371 let clamped = interpolate_age_stages(&a, &b, 2.0);
372 assert!((clamped.fat_pct - b.fat_pct).abs() < 1e-5);
373 }
374
375 #[test]
376 fn test_compute_age_stage_clamps_low() {
377 let curve = default_aging_curve_male();
378 let stage = compute_age_stage(&curve, 5.0);
379 assert!((stage.years - 10.0).abs() < 1e-3);
380 }
381
382 #[test]
383 fn test_compute_age_stage_clamps_high() {
384 let curve = default_aging_curve_male();
385 let stage = compute_age_stage(&curve, 100.0);
386 assert!((stage.years - 90.0).abs() < 1e-3);
387 }
388
389 #[test]
390 fn test_bone_density_decreases_with_age() {
391 let curve = default_aging_curve_male();
392 let young = compute_age_stage(&curve, 30.0);
393 let old = compute_age_stage(&curve, 70.0);
394 assert!(old.bone_density < young.bone_density);
395 }
396
397 #[test]
398 fn test_skin_elasticity_decreases_with_age() {
399 let curve = default_aging_curve_female();
400 let young = compute_age_stage(&curve, 20.0);
401 let old = compute_age_stage(&curve, 80.0);
402 assert!(old.skin_elasticity < young.skin_elasticity);
403 }
404
405 #[test]
406 fn test_male_vs_female_differ() {
407 let male = default_aging_curve_male();
408 let female = default_aging_curve_female();
409 let m50 = compute_age_stage(&male, 50.0);
410 let f50 = compute_age_stage(&female, 50.0);
411 assert!(f50.fat_pct > m50.fat_pct);
413 }
414
415 #[test]
416 fn test_skin_aging_params_non_empty() {
417 let params = skin_aging_params(60.0, 0);
418 assert!(!params.is_empty());
419 }
420
421 #[test]
422 fn test_skin_aging_params_young_all_near_zero() {
423 let params = skin_aging_params(20.0, 0);
424 for (_, v) in ¶ms {
425 assert!(*v >= 0.0);
426 }
427 }
428
429 #[test]
430 fn test_face_aging_params_non_empty() {
431 let params = face_aging_params(50.0);
432 assert!(!params.is_empty());
433 for (_, v) in ¶ms {
434 assert!(*v >= 0.0);
435 }
436 }
437
438 #[test]
439 fn test_body_aging_params_non_empty() {
440 let params = body_aging_params(65.0, 0);
441 assert!(!params.is_empty());
442 }
443
444 #[test]
445 fn test_simulate_aging_non_empty() {
446 let curve = default_aging_curve_male();
447 let profile = AgeProfile {
448 base_age: 25.0,
449 sex: 0,
450 ethnicity: 0,
451 };
452 let params = simulate_aging(&profile, 65.0, &curve);
453 assert!(!params.is_empty());
454 }
455
456 #[test]
457 fn test_reverse_aging_negates() {
458 let params = vec![
459 ("wrinkle_forehead".into(), 0.5f32),
460 ("sag_cheeks".into(), 0.3f32),
461 ];
462 let reversed = reverse_aging(¶ms);
463 for (orig, rev) in params.iter().zip(reversed.iter()) {
464 assert!((orig.1 + rev.1).abs() < 1e-6);
465 }
466 }
467
468 #[test]
469 fn test_age_progression_deltas_count() {
470 let curve = default_aging_curve_male();
471 let from = compute_age_stage(&curve, 30.0);
472 let to = compute_age_stage(&curve, 70.0);
473 let deltas = age_progression_deltas(&from, &to);
474 assert_eq!(deltas.len(), 4);
475 }
476
477 #[test]
478 fn test_default_curves_have_10_stages() {
479 assert_eq!(default_aging_curve_male().stages.len(), 10);
480 assert_eq!(default_aging_curve_female().stages.len(), 10);
481 }
482
483 #[test]
484 fn test_simulate_aging_backward_gives_negatives() {
485 let curve = default_aging_curve_male();
486 let profile = AgeProfile {
487 base_age: 60.0,
488 sex: 0,
489 ethnicity: 0,
490 };
491 let params = simulate_aging(&profile, 30.0, &curve);
493 let fat_delta = params
494 .iter()
495 .find(|(k, _)| k == "delta_fat_pct")
496 .expect("should succeed");
497 assert!(fat_delta.1 < 0.0);
498 }
499}