1use std::collections::HashMap;
7
8#[allow(dead_code)]
14#[derive(Debug, Clone)]
15pub struct AnthroConstraint {
16 pub name: String,
17 pub description: String,
18 pub min_ratio: f32,
19 pub max_ratio: f32,
20}
21
22#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct AnthroConstraintSet {
26 pub constraints: Vec<AnthroConstraint>,
27}
28
29#[allow(dead_code)]
31#[derive(Debug, Clone)]
32pub struct AnthroViolation {
33 pub constraint_name: String,
34 pub actual_ratio: f32,
35 pub min_ratio: f32,
36 pub max_ratio: f32,
37 pub severity: f32,
39}
40
41#[allow(dead_code)]
43#[derive(Debug, Clone)]
44pub struct AnthroCheckResult {
45 pub violations: Vec<AnthroViolation>,
46 pub is_realistic: bool,
47 pub realism_score: f32,
48}
49
50#[allow(dead_code)]
56pub fn standard_anthropometric_constraints() -> AnthroConstraintSet {
57 let constraints = vec![
58 AnthroConstraint {
59 name: "head_height_to_body".into(),
60 description: "Head height as fraction of total body height (1/6 to 1/8)".into(),
61 min_ratio: 0.11,
62 max_ratio: 0.17,
63 },
64 AnthroConstraint {
65 name: "shoulder_to_hip_width".into(),
66 description: "Shoulder width relative to hip width".into(),
67 min_ratio: 0.8,
68 max_ratio: 1.5,
69 },
70 AnthroConstraint {
71 name: "arm_span_to_height".into(),
72 description: "Arm span approximately equal to height".into(),
73 min_ratio: 0.95,
74 max_ratio: 1.05,
75 },
76 AnthroConstraint {
77 name: "leg_to_torso".into(),
78 description: "Leg length relative to torso length".into(),
79 min_ratio: 0.9,
80 max_ratio: 1.2,
81 },
82 AnthroConstraint {
83 name: "bmi_realistic".into(),
84 description: "Body mass index realistic range".into(),
85 min_ratio: 15.0,
86 max_ratio: 45.0,
87 },
88 AnthroConstraint {
89 name: "foot_to_height".into(),
90 description: "Foot length as fraction of total height".into(),
91 min_ratio: 0.13,
92 max_ratio: 0.17,
93 },
94 AnthroConstraint {
95 name: "hand_to_forearm".into(),
96 description: "Hand length relative to forearm length".into(),
97 min_ratio: 0.6,
98 max_ratio: 0.8,
99 },
100 AnthroConstraint {
101 name: "neck_to_head".into(),
102 description: "Neck circumference relative to head circumference".into(),
103 min_ratio: 0.3,
104 max_ratio: 0.5,
105 },
106 ];
107 AnthroConstraintSet { constraints }
108}
109
110#[allow(dead_code)]
116pub fn bmi_from_params(height_m: f32, weight_kg: f32) -> f32 {
117 if height_m <= 0.0 {
118 return 0.0;
119 }
120 weight_kg / (height_m * height_m)
121}
122
123#[allow(dead_code)]
125pub fn violation_severity(actual: f32, min: f32, max: f32) -> f32 {
126 let range = (max - min).max(f32::EPSILON);
127 if actual < min {
128 ((min - actual) / range).clamp(0.0, 1.0)
129 } else if actual > max {
130 ((actual - max) / range).clamp(0.0, 1.0)
131 } else {
132 0.0
133 }
134}
135
136#[allow(dead_code)]
138pub fn realism_score(violations: &[AnthroViolation]) -> f32 {
139 if violations.is_empty() {
140 return 1.0;
141 }
142 let mean_sev = violations.iter().map(|v| v.severity).sum::<f32>() / violations.len() as f32;
143 (1.0 - mean_sev).clamp(0.0, 1.0)
144}
145
146#[allow(dead_code)]
153pub fn params_to_body_ratios(params: &HashMap<String, f32>) -> HashMap<String, f32> {
154 let get = |k: &str| -> Option<f32> { params.get(k).copied().filter(|&v| v > 0.0) };
155
156 let mut ratios = HashMap::new();
157
158 if let (Some(head_h), Some(height)) = (get("head_height"), get("height")) {
159 ratios.insert("head_height_to_body".into(), head_h / height);
160 }
161 if let (Some(sw), Some(hw)) = (get("shoulder_width"), get("hip_width")) {
162 ratios.insert("shoulder_to_hip_width".into(), sw / hw);
163 }
164 if let (Some(span), Some(height)) = (get("arm_span"), get("height")) {
165 ratios.insert("arm_span_to_height".into(), span / height);
166 }
167 if let (Some(leg), Some(torso)) = (get("leg_length"), get("torso_length")) {
168 ratios.insert("leg_to_torso".into(), leg / torso);
169 }
170 if let (Some(h), Some(w)) = (get("height"), get("weight")) {
171 ratios.insert("bmi_realistic".into(), bmi_from_params(h, w));
172 }
173 if let (Some(foot), Some(height)) = (get("foot_length"), get("height")) {
174 ratios.insert("foot_to_height".into(), foot / height);
175 }
176 if let (Some(hand), Some(fore)) = (get("hand_length"), get("forearm_length")) {
177 ratios.insert("hand_to_forearm".into(), hand / fore);
178 }
179 if let (Some(neck), Some(head_c)) = (get("neck_circ"), get("head_circ")) {
180 ratios.insert("neck_to_head".into(), neck / head_c);
181 }
182
183 ratios
184}
185
186#[allow(dead_code)]
188pub fn check_params_against_constraints(
189 params: &HashMap<String, f32>,
190 constraints: &AnthroConstraintSet,
191) -> AnthroCheckResult {
192 let ratios = params_to_body_ratios(params);
193 let mut violations = Vec::new();
194
195 for c in &constraints.constraints {
196 let actual = match ratios.get(&c.name) {
197 Some(&v) => v,
198 None => continue,
199 };
200 let sev = violation_severity(actual, c.min_ratio, c.max_ratio);
201 if sev > 0.0 {
202 violations.push(AnthroViolation {
203 constraint_name: c.name.clone(),
204 actual_ratio: actual,
205 min_ratio: c.min_ratio,
206 max_ratio: c.max_ratio,
207 severity: sev,
208 });
209 }
210 }
211
212 let score = realism_score(&violations);
213 AnthroCheckResult {
214 is_realistic: violations.is_empty(),
215 violations,
216 realism_score: score,
217 }
218}
219
220#[allow(dead_code)]
225pub fn enforce_constraints(
226 params: &mut HashMap<String, f32>,
227 constraints: &AnthroConstraintSet,
228) -> usize {
229 let mut clamped = 0usize;
230
231 for c in &constraints.constraints {
232 match c.name.as_str() {
233 "bmi_realistic" => {
234 let height = params.get("height").copied().unwrap_or(0.0);
235 if height <= 0.0 {
236 continue;
237 }
238 if let Some(weight) = params.get_mut("weight") {
239 let bmi = *weight / (height * height);
240 if bmi < c.min_ratio {
241 *weight = c.min_ratio * height * height;
242 clamped += 1;
243 } else if bmi > c.max_ratio {
244 *weight = c.max_ratio * height * height;
245 clamped += 1;
246 }
247 }
248 }
249 "head_height_to_body" => {
250 clamp_ratio_numerator(params, "head_height", "height", c, &mut clamped);
251 }
252 "shoulder_to_hip_width" => {
253 clamp_ratio_numerator(params, "shoulder_width", "hip_width", c, &mut clamped);
254 }
255 "arm_span_to_height" => {
256 clamp_ratio_numerator(params, "arm_span", "height", c, &mut clamped);
257 }
258 "leg_to_torso" => {
259 clamp_ratio_numerator(params, "leg_length", "torso_length", c, &mut clamped);
260 }
261 "foot_to_height" => {
262 clamp_ratio_numerator(params, "foot_length", "height", c, &mut clamped);
263 }
264 "hand_to_forearm" => {
265 clamp_ratio_numerator(params, "hand_length", "forearm_length", c, &mut clamped);
266 }
267 "neck_to_head" => {
268 clamp_ratio_numerator(params, "neck_circ", "head_circ", c, &mut clamped);
269 }
270 _ => {}
271 }
272 }
273
274 clamped
275}
276
277fn clamp_ratio_numerator(
279 params: &mut HashMap<String, f32>,
280 num_key: &str,
281 den_key: &str,
282 c: &AnthroConstraint,
283 clamped: &mut usize,
284) {
285 let denom = params.get(den_key).copied().unwrap_or(0.0);
286 if denom <= 0.0 {
287 return;
288 }
289 if let Some(num) = params.get_mut(num_key) {
290 let ratio = *num / denom;
291 if ratio < c.min_ratio {
292 *num = c.min_ratio * denom;
293 *clamped += 1;
294 } else if ratio > c.max_ratio {
295 *num = c.max_ratio * denom;
296 *clamped += 1;
297 }
298 }
299}
300
301#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn typical_human() -> HashMap<String, f32> {
310 let mut m = HashMap::new();
311 m.insert("height".into(), 1.75);
312 m.insert("weight".into(), 70.0);
313 m.insert("head_height".into(), 0.23); m.insert("shoulder_width".into(), 0.46);
315 m.insert("hip_width".into(), 0.38);
316 m.insert("arm_span".into(), 1.76);
317 m.insert("leg_length".into(), 0.95);
318 m.insert("torso_length".into(), 0.85);
319 m.insert("foot_length".into(), 0.26); m.insert("hand_length".into(), 0.14);
321 m.insert("forearm_length".into(), 0.20);
322 m.insert("neck_circ".into(), 0.13);
323 m.insert("head_circ".into(), 0.38);
324 m
325 }
326
327 #[test]
328 fn test_bmi_from_params_normal() {
329 let bmi = bmi_from_params(1.75, 70.0);
330 assert!((bmi - 22.857).abs() < 0.01, "bmi={bmi}");
331 }
332
333 #[test]
334 fn test_bmi_from_params_zero_height() {
335 assert_eq!(bmi_from_params(0.0, 70.0), 0.0);
336 }
337
338 #[test]
339 fn test_realism_score_no_violations() {
340 let score = realism_score(&[]);
341 assert_eq!(score, 1.0);
342 }
343
344 #[test]
345 fn test_realism_score_all_severe() {
346 let vs = vec![
347 AnthroViolation {
348 constraint_name: "a".into(),
349 actual_ratio: 0.0,
350 min_ratio: 0.0,
351 max_ratio: 0.0,
352 severity: 1.0,
353 },
354 AnthroViolation {
355 constraint_name: "b".into(),
356 actual_ratio: 0.0,
357 min_ratio: 0.0,
358 max_ratio: 0.0,
359 severity: 1.0,
360 },
361 ];
362 assert_eq!(realism_score(&vs), 0.0);
363 }
364
365 #[test]
366 fn test_violation_severity_in_bounds() {
367 assert_eq!(violation_severity(0.5, 0.3, 0.7), 0.0);
368 }
369
370 #[test]
371 fn test_violation_severity_below_min() {
372 let sev = violation_severity(0.1, 0.3, 0.7);
373 assert!(sev > 0.0);
374 assert!(sev <= 1.0);
375 }
376
377 #[test]
378 fn test_violation_severity_above_max() {
379 let sev = violation_severity(0.9, 0.3, 0.7);
380 assert!(sev > 0.0);
381 assert!(sev <= 1.0);
382 }
383
384 #[test]
385 fn test_violation_severity_at_min_boundary() {
386 assert_eq!(violation_severity(0.3, 0.3, 0.7), 0.0);
387 }
388
389 #[test]
390 fn test_violation_severity_at_max_boundary() {
391 assert_eq!(violation_severity(0.7, 0.3, 0.7), 0.0);
392 }
393
394 #[test]
395 fn test_standard_constraints_at_least_8() {
396 let cs = standard_anthropometric_constraints();
397 assert!(cs.constraints.len() >= 8, "len={}", cs.constraints.len());
398 }
399
400 #[test]
401 fn test_check_valid_human_no_violations() {
402 let params = typical_human();
403 let cs = standard_anthropometric_constraints();
404 let result = check_params_against_constraints(¶ms, &cs);
405 assert!(
406 result.is_realistic,
407 "Expected no violations, got: {:?}",
408 result.violations
409 );
410 assert!(result.realism_score > 0.9);
411 }
412
413 #[test]
414 fn test_check_extreme_params_have_violations() {
415 let mut params = HashMap::new();
416 params.insert("height".into(), 1.0);
417 params.insert("weight".into(), 200.0); params.insert("head_height".into(), 0.5); params.insert("hip_width".into(), 0.3);
420 params.insert("shoulder_width".into(), 0.06); let cs = standard_anthropometric_constraints();
422 let result = check_params_against_constraints(¶ms, &cs);
423 assert!(!result.is_realistic);
424 assert!(!result.violations.is_empty());
425 }
426
427 #[test]
428 fn test_enforce_constraints_clamps_bmi() {
429 let mut params = HashMap::new();
430 params.insert("height".into(), 1.75);
431 params.insert("weight".into(), 300.0); let cs = standard_anthropometric_constraints();
433 let count = enforce_constraints(&mut params, &cs);
434 assert!(count >= 1);
435 let bmi = bmi_from_params(1.75, *params.get("weight").expect("should succeed"));
436 assert!(bmi <= 45.0 + 0.001);
437 }
438
439 #[test]
440 fn test_params_to_body_ratios_returns_map() {
441 let params = typical_human();
442 let ratios = params_to_body_ratios(¶ms);
443 assert!(!ratios.is_empty());
444 assert!(ratios.contains_key("bmi_realistic"));
445 assert!(ratios.contains_key("head_height_to_body"));
446 }
447
448 #[test]
449 fn test_params_to_body_ratios_empty_params() {
450 let params = HashMap::new();
451 let ratios = params_to_body_ratios(¶ms);
452 assert!(ratios.is_empty());
453 }
454
455 #[test]
456 fn test_realism_score_partial() {
457 let vs = vec![AnthroViolation {
458 constraint_name: "x".into(),
459 actual_ratio: 0.0,
460 min_ratio: 0.0,
461 max_ratio: 0.0,
462 severity: 0.5,
463 }];
464 let s = realism_score(&vs);
465 assert!((s - 0.5).abs() < 1e-5);
466 }
467}