1#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct BodyComposition {
12 pub fat_pct: f32,
14 pub muscle_pct: f32,
16 pub bone_pct: f32,
18 pub water_pct: f32,
20}
21
22#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct CompositionProfile {
26 pub sex: u8,
28 pub age: f32,
29 pub height_m: f32,
30 pub weight_kg: f32,
31 pub composition: BodyComposition,
32}
33
34#[allow(dead_code)]
38pub fn validate_composition(comp: &BodyComposition) -> bool {
39 let parts = [comp.fat_pct, comp.muscle_pct, comp.bone_pct, comp.water_pct];
40 if parts.iter().any(|v| !(0.0..=1.0).contains(v)) {
41 return false;
42 }
43 let sum: f32 = parts.iter().sum();
44 (sum - 1.0).abs() < 0.01
45}
46
47#[allow(dead_code)]
49pub fn bmi(height_m: f32, weight_kg: f32) -> f32 {
50 if height_m < 0.01 {
51 return 0.0;
52 }
53 weight_kg / (height_m * height_m)
54}
55
56#[allow(dead_code)]
59pub fn body_fat_from_bmi_sex_age(bmi_val: f32, sex: u8, age_years: f32) -> f32 {
60 let sex_factor = if sex == 0 { 1.0_f32 } else { 0.0_f32 };
63 let bf = 1.20 * bmi_val + 0.23 * age_years - 10.8 * sex_factor - 5.4;
64 (bf / 100.0).clamp(0.02, 0.65)
65}
66
67#[allow(dead_code)]
69pub fn lean_mass_kg(weight_kg: f32, fat_pct: f32) -> f32 {
70 weight_kg * (1.0 - fat_pct.clamp(0.0, 1.0))
71}
72
73#[allow(dead_code)]
75pub fn fat_mass_kg(weight_kg: f32, fat_pct: f32) -> f32 {
76 weight_kg * fat_pct.clamp(0.0, 1.0)
77}
78
79#[allow(dead_code)]
82pub fn classify_body_fat(fat_pct: f32, sex: u8) -> &'static str {
83 if sex == 0 {
84 if fat_pct < 0.05 {
86 "essential"
87 } else if fat_pct < 0.14 {
88 "athletic"
89 } else if fat_pct < 0.18 {
90 "fitness"
91 } else if fat_pct < 0.25 {
92 "average"
93 } else {
94 "obese"
95 }
96 } else {
97 if fat_pct < 0.10 {
99 "essential"
100 } else if fat_pct < 0.21 {
101 "athletic"
102 } else if fat_pct < 0.25 {
103 "fitness"
104 } else if fat_pct < 0.32 {
105 "average"
106 } else {
107 "obese"
108 }
109 }
110}
111
112#[allow(dead_code)]
115pub fn ideal_weight_devine(height_m: f32, sex: u8) -> f32 {
116 let height_cm = height_m * 100.0;
117 let inches_over_5ft = ((height_cm / 2.54) - 60.0).max(0.0);
118 if sex == 0 {
119 50.0 + 2.3 * inches_over_5ft
120 } else {
121 45.5 + 2.3 * inches_over_5ft
122 }
123}
124
125#[allow(dead_code)]
127pub fn ffmi(lean_mass_kg_val: f32, height_m: f32) -> f32 {
128 if height_m < 0.01 {
129 return 0.0;
130 }
131 lean_mass_kg_val / (height_m * height_m)
132}
133
134#[allow(dead_code)]
138pub fn morph_params_from_composition(comp: &BodyComposition) -> Vec<(String, f32)> {
139 vec![
140 ("fat-torso".to_string(), comp.fat_pct),
141 ("fat-arms".to_string(), comp.fat_pct * 0.7),
142 ("fat-legs".to_string(), comp.fat_pct * 0.8),
143 ("muscle-torso".to_string(), comp.muscle_pct),
144 (
145 "muscle-arms".to_string(),
146 comp.muscle_pct * 1.1_f32.min(1.0),
147 ),
148 ("muscle-legs".to_string(), comp.muscle_pct * 0.9),
149 ("bone-mass".to_string(), comp.bone_pct),
150 ]
151}
152
153#[allow(dead_code)]
155pub fn composition_from_morph_params(params: &[(String, f32)]) -> BodyComposition {
156 let get = |key: &str| -> f32 {
157 params
158 .iter()
159 .find(|(k, _)| k == key)
160 .map_or(0.0, |(_, v)| *v)
161 };
162 let fat_pct = get("fat-torso").clamp(0.0, 1.0);
163 let muscle_pct = get("muscle-torso").clamp(0.0, 1.0);
164 let bone_pct = get("bone-mass").clamp(0.0, 1.0);
165 let rest = (1.0_f32 - fat_pct - muscle_pct - bone_pct).max(0.0);
166 BodyComposition {
167 fat_pct,
168 muscle_pct,
169 bone_pct,
170 water_pct: rest,
171 }
172}
173
174#[allow(dead_code)]
176pub fn interpolate_compositions(
177 a: &BodyComposition,
178 b: &BodyComposition,
179 t: f32,
180) -> BodyComposition {
181 let lerp = |x: f32, y: f32| x + t * (y - x);
182 BodyComposition {
183 fat_pct: lerp(a.fat_pct, b.fat_pct),
184 muscle_pct: lerp(a.muscle_pct, b.muscle_pct),
185 bone_pct: lerp(a.bone_pct, b.bone_pct),
186 water_pct: lerp(a.water_pct, b.water_pct),
187 }
188}
189
190#[allow(dead_code)]
192pub fn composition_distance(a: &BodyComposition, b: &BodyComposition) -> f32 {
193 let df = a.fat_pct - b.fat_pct;
194 let dm = a.muscle_pct - b.muscle_pct;
195 let db = a.bone_pct - b.bone_pct;
196 let dw = a.water_pct - b.water_pct;
197 (df * df + dm * dm + db * db + dw * dw).sqrt()
198}
199
200#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn valid_comp() -> BodyComposition {
207 BodyComposition {
208 fat_pct: 0.20,
209 muscle_pct: 0.45,
210 bone_pct: 0.15,
211 water_pct: 0.20,
212 }
213 }
214
215 #[test]
216 fn test_validate_composition_valid() {
217 assert!(validate_composition(&valid_comp()));
218 }
219
220 #[test]
221 fn test_validate_composition_over_1() {
222 let c = BodyComposition {
223 fat_pct: 0.5,
224 muscle_pct: 0.5,
225 bone_pct: 0.5,
226 water_pct: 0.5,
227 };
228 assert!(!validate_composition(&c));
229 }
230
231 #[test]
232 fn test_validate_composition_negative() {
233 let c = BodyComposition {
234 fat_pct: -0.1,
235 muscle_pct: 0.5,
236 bone_pct: 0.2,
237 water_pct: 0.4,
238 };
239 assert!(!validate_composition(&c));
240 }
241
242 #[test]
243 fn test_bmi_formula() {
244 let b = bmi(1.8, 81.0);
245 assert!((b - 25.0).abs() < 0.1);
246 }
247
248 #[test]
249 fn test_bmi_zero_height() {
250 assert_eq!(bmi(0.0, 80.0), 0.0);
251 }
252
253 #[test]
254 fn test_body_fat_no_nan() {
255 let bf = body_fat_from_bmi_sex_age(25.0, 0, 30.0);
256 assert!(!bf.is_nan());
257 }
258
259 #[test]
260 fn test_body_fat_clamped() {
261 let bf = body_fat_from_bmi_sex_age(25.0, 0, 30.0);
262 assert!((0.02..=0.65).contains(&bf));
263 }
264
265 #[test]
266 fn test_classify_fat_male_athletic() {
267 assert_eq!(classify_body_fat(0.10, 0), "athletic");
268 }
269
270 #[test]
271 fn test_classify_fat_female_obese() {
272 assert_eq!(classify_body_fat(0.38, 1), "obese");
273 }
274
275 #[test]
276 fn test_classify_fat_male_essential() {
277 assert_eq!(classify_body_fat(0.03, 0), "essential");
278 }
279
280 #[test]
281 fn test_ideal_weight_sex_difference() {
282 let male = ideal_weight_devine(1.75, 0);
283 let female = ideal_weight_devine(1.75, 1);
284 assert!(male > female);
285 }
286
287 #[test]
288 fn test_ideal_weight_devine_known() {
289 let w = ideal_weight_devine(1.7526, 0);
291 assert!((w - 70.7).abs() < 1.0);
292 }
293
294 #[test]
295 fn test_ffmi_range() {
296 let f = ffmi(70.0, 1.8);
297 assert!(f > 10.0 && f < 30.0);
298 }
299
300 #[test]
301 fn test_interpolate_t0() {
302 let a = valid_comp();
303 let b = BodyComposition {
304 fat_pct: 0.30,
305 muscle_pct: 0.40,
306 bone_pct: 0.10,
307 water_pct: 0.20,
308 };
309 let out = interpolate_compositions(&a, &b, 0.0);
310 assert!((out.fat_pct - a.fat_pct).abs() < 1e-6);
311 }
312
313 #[test]
314 fn test_interpolate_t1() {
315 let a = valid_comp();
316 let b = BodyComposition {
317 fat_pct: 0.30,
318 muscle_pct: 0.40,
319 bone_pct: 0.10,
320 water_pct: 0.20,
321 };
322 let out = interpolate_compositions(&a, &b, 1.0);
323 assert!((out.fat_pct - b.fat_pct).abs() < 1e-6);
324 }
325
326 #[test]
327 fn test_composition_distance_zero_same() {
328 let a = valid_comp();
329 let d = composition_distance(&a, &a);
330 assert!(d.abs() < 1e-6);
331 }
332
333 #[test]
334 fn test_composition_distance_positive_different() {
335 let a = valid_comp();
336 let b = BodyComposition {
337 fat_pct: 0.30,
338 muscle_pct: 0.40,
339 bone_pct: 0.10,
340 water_pct: 0.20,
341 };
342 assert!(composition_distance(&a, &b) > 0.0);
343 }
344
345 #[test]
346 fn test_lean_mass_kg() {
347 assert!((lean_mass_kg(80.0, 0.20) - 64.0).abs() < 1e-4);
348 }
349
350 #[test]
351 fn test_fat_mass_kg() {
352 assert!((fat_mass_kg(80.0, 0.20) - 16.0).abs() < 1e-4);
353 }
354}