#!/usr/bin/env python3 """ who_growth.py — WHO Child Growth Standards (Girls 0–24 Months) Deterministic percentile and z-score calculations via LMS method and reference curve data for Mila's growth tracking. """ from __future__ import annotations import math from typing import Any, Dict, List, Optional, Tuple # WHO Child Growth Standards (Girls: Birth to 24 Months) # Source: World Health Organization (WHO) Multi-centre Growth Reference Study (MGRS) # https://www.who.int/tools/child-growth-standards/standards WHO_GIRLS_WEIGHT = [ {"month": 0, "L": 0.3809, "M": 3.2322, "S": 0.14171, "p3": 2.4, "p50": 3.2, "p97": 4.2}, {"month": 1, "L": 0.1714, "M": 4.1873, "S": 0.13724, "p3": 3.2, "p50": 4.2, "p97": 5.4}, {"month": 2, "L": 0.0962, "M": 5.1282, "S": 0.13000, "p3": 4.0, "p50": 5.1, "p97": 6.5}, {"month": 3, "L": 0.0402, "M": 5.8458, "S": 0.12619, "p3": 4.6, "p50": 5.8, "p97": 7.4}, {"month": 4, "L": -0.0050, "M": 6.4237, "S": 0.12402, "p3": 5.1, "p50": 6.4, "p97": 8.1}, {"month": 5, "L": -0.0430, "M": 6.8985, "S": 0.12274, "p3": 5.5, "p50": 6.9, "p97": 8.7}, {"month": 6, "L": -0.0756, "M": 7.2970, "S": 0.12204, "p3": 5.8, "p50": 7.3, "p97": 9.2}, {"month": 7, "L": -0.1039, "M": 7.6422, "S": 0.12178, "p3": 6.1, "p50": 7.6, "p97": 9.6}, {"month": 8, "L": -0.1288, "M": 7.9487, "S": 0.12181, "p3": 6.3, "p50": 7.9, "p97": 10.0}, {"month": 9, "L": -0.1507, "M": 8.2254, "S": 0.12199, "p3": 6.6, "p50": 8.2, "p97": 10.4}, {"month": 10, "L": -0.1700, "M": 8.4800, "S": 0.12223, "p3": 6.8, "p50": 8.5, "p97": 10.7}, {"month": 11, "L": -0.1872, "M": 8.7192, "S": 0.12247, "p3": 7.0, "p50": 8.7, "p97": 11.0}, {"month": 12, "L": -0.2024, "M": 8.9481, "S": 0.12268, "p3": 7.1, "p50": 8.9, "p97": 11.3}, {"month": 13, "L": -0.2158, "M": 9.1699, "S": 0.12283, "p3": 7.3, "p50": 9.2, "p97": 11.6}, {"month": 14, "L": -0.2278, "M": 9.3870, "S": 0.12294, "p3": 7.5, "p50": 9.4, "p97": 11.9}, {"month": 15, "L": -0.2384, "M": 9.6008, "S": 0.12299, "p3": 7.7, "p50": 9.6, "p97": 12.2}, {"month": 16, "L": -0.2478, "M": 9.8124, "S": 0.12303, "p3": 7.8, "p50": 9.8, "p97": 12.5}, {"month": 17, "L": -0.2562, "M": 10.0226, "S": 0.12306, "p3": 8.0, "p50": 10.0, "p97": 12.7}, {"month": 18, "L": -0.2637, "M": 10.2315, "S": 0.12309, "p3": 8.2, "p50": 10.2, "p97": 13.0}, {"month": 19, "L": -0.2703, "M": 10.4393, "S": 0.12315, "p3": 8.3, "p50": 10.4, "p97": 13.3}, {"month": 20, "L": -0.2762, "M": 10.6464, "S": 0.12323, "p3": 8.5, "p50": 10.6, "p97": 13.5}, {"month": 21, "L": -0.2815, "M": 10.8534, "S": 0.12335, "p3": 8.7, "p50": 10.9, "p97": 13.8}, {"month": 22, "L": -0.2862, "M": 11.0608, "S": 0.12350, "p3": 8.8, "p50": 11.1, "p97": 14.1}, {"month": 23, "L": -0.2903, "M": 11.2688, "S": 0.12369, "p3": 9.0, "p50": 11.3, "p97": 14.3}, {"month": 24, "L": -0.2941, "M": 11.4775, "S": 0.12390, "p3": 9.2, "p50": 11.5, "p97": 14.6}, ] WHO_GIRLS_HEIGHT = [ {"month": 0, "L": 1.0, "M": 49.1477, "S": 0.03790, "p3": 45.6, "p50": 49.1, "p97": 52.7}, {"month": 1, "L": 1.0, "M": 53.6872, "S": 0.03640, "p3": 50.0, "p50": 53.7, "p97": 57.4}, {"month": 2, "L": 1.0, "M": 57.0673, "S": 0.03568, "p3": 53.2, "p50": 57.1, "p97": 60.9}, {"month": 3, "L": 1.0, "M": 59.8029, "S": 0.03520, "p3": 55.8, "p50": 59.8, "p97": 63.8}, {"month": 4, "L": 1.0, "M": 62.0899, "S": 0.03486, "p3": 58.0, "p50": 62.1, "p97": 66.2}, {"month": 5, "L": 1.0, "M": 64.0301, "S": 0.03463, "p3": 59.9, "p50": 64.0, "p97": 68.2}, {"month": 6, "L": 1.0, "M": 65.7311, "S": 0.03448, "p3": 61.5, "p50": 65.7, "p97": 70.0}, {"month": 7, "L": 1.0, "M": 67.2873, "S": 0.03441, "p3": 62.9, "p50": 67.3, "p97": 71.6}, {"month": 8, "L": 1.0, "M": 68.7498, "S": 0.03440, "p3": 64.3, "p50": 68.7, "p97": 73.2}, {"month": 9, "L": 1.0, "M": 70.1435, "S": 0.03444, "p3": 65.6, "p50": 70.1, "p97": 74.7}, {"month": 10, "L": 1.0, "M": 71.4818, "S": 0.03452, "p3": 66.8, "p50": 71.5, "p97": 76.1}, {"month": 11, "L": 1.0, "M": 72.7710, "S": 0.03464, "p3": 68.0, "p50": 72.8, "p97": 77.5}, {"month": 12, "L": 1.0, "M": 74.0150, "S": 0.03479, "p3": 69.2, "p50": 74.0, "p97": 78.9}, {"month": 13, "L": 1.0, "M": 75.2176, "S": 0.03496, "p3": 70.3, "p50": 75.2, "p97": 80.2}, {"month": 14, "L": 1.0, "M": 76.3817, "S": 0.03514, "p3": 71.3, "p50": 76.4, "p97": 81.4}, {"month": 15, "L": 1.0, "M": 77.5099, "S": 0.03534, "p3": 72.4, "p50": 77.5, "p97": 82.7}, {"month": 16, "L": 1.0, "M": 78.6055, "S": 0.03555, "p3": 73.3, "p50": 78.6, "p97": 83.9}, {"month": 17, "L": 1.0, "M": 79.6710, "S": 0.03576, "p3": 74.3, "p50": 79.7, "p97": 85.0}, {"month": 18, "L": 1.0, "M": 80.7079, "S": 0.03598, "p3": 75.2, "p50": 80.7, "p97": 86.2}, {"month": 19, "L": 1.0, "M": 81.7182, "S": 0.03620, "p3": 76.2, "p50": 81.7, "p97": 87.3}, {"month": 20, "L": 1.0, "M": 82.7036, "S": 0.03643, "p3": 77.0, "p50": 82.7, "p97": 88.4}, {"month": 21, "L": 1.0, "M": 83.6654, "S": 0.03666, "p3": 77.9, "p50": 83.7, "p97": 89.4}, {"month": 22, "L": 1.0, "M": 84.6040, "S": 0.03688, "p3": 78.7, "p50": 84.6, "p97": 90.5}, {"month": 23, "L": 1.0, "M": 85.5202, "S": 0.03711, "p3": 79.6, "p50": 85.5, "p97": 91.5}, {"month": 24, "L": 1.0, "M": 86.4153, "S": 0.03734, "p3": 80.3, "p50": 86.4, "p97": 92.5}, ] def _interpolate_lms(table: List[Dict[str, float]], age_months: float) -> Tuple[float, float, float, float, float, float]: """Linearly interpolates L, M, S, p3, p50, p97 for a given age in months.""" m_clamped = max(0.0, min(float(table[-1]["month"]), float(age_months))) m_low = int(math.floor(m_clamped)) m_high = int(math.ceil(m_clamped)) r_low = table[m_low] if m_low == m_high: return r_low["L"], r_low["M"], r_low["S"], r_low["p3"], r_low["p50"], r_low["p97"] r_high = table[m_high] frac = m_clamped - m_low L = r_low["L"] + frac * (r_high["L"] - r_low["L"]) M = r_low["M"] + frac * (r_high["M"] - r_low["M"]) S = r_low["S"] + frac * (r_high["S"] - r_low["S"]) p3 = r_low["p3"] + frac * (r_high["p3"] - r_low["p3"]) p50 = r_low["p50"] + frac * (r_high["p50"] - r_low["p50"]) p97 = r_low["p97"] + frac * (r_high["p97"] - r_low["p97"]) return L, M, S, p3, p50, p97 def calculate_percentile(metric: str, value: float, age_months: float) -> Dict[str, Any]: """ Computes exact WHO z-score and percentile using the LMS formula. metric: 'weight' (in kg) or 'height' (in cm) """ table = WHO_GIRLS_WEIGHT if metric == "weight" else WHO_GIRLS_HEIGHT L, M, S, p3, p50, p97 = _interpolate_lms(table, age_months) if value <= 0 or M <= 0 or S <= 0: return { "value": value, "age_months": round(age_months, 2), "z_score": 0.0, "percentile": 50.0, "percentile_str": "P50", "p3": round(p3, 2), "p50": round(p50, 2), "p97": round(p97, 2), "in_normal_range": True, "status": "normal", } # Box-Cox power exponential LMS formula if abs(L) > 1e-6: z = ((value / M) ** L - 1.0) / (L * S) else: z = math.log(value / M) / S # Cumulative standard normal distribution p = 0.5 * (1.0 + math.erf(z / math.sqrt(2.0))) * 100.0 p_clamped = max(0.1, min(99.9, p)) in_normal = 3.0 <= p_clamped <= 97.0 if p_clamped < 3.0: status = "unter P3" elif p_clamped > 97.0: status = "über P97" else: status = "im WHO-Normbereich" return { "value": value, "age_months": round(age_months, 2), "z_score": round(z, 2), "percentile": round(p_clamped, 1), "percentile_str": f"P{int(round(p_clamped))}", "p3": round(p3, 2), "p50": round(p50, 2), "p97": round(p97, 2), "in_normal_range": in_normal, "status": status, } def format_growth_feedback( weight_kg: Optional[float], height_cm: Optional[float], age_months: float, date_str: str, ) -> str: """Formats clear, comforting feedback for CLI and Hebamme agent responses.""" parts = [] if weight_kg is not None: pw = calculate_percentile("weight", weight_kg, age_months) parts.append(f"{weight_kg:.2f} kg ({pw['percentile_str']} · WHO Median: {pw['p50']:.1f} kg)") if height_cm is not None: ph = calculate_percentile("height", height_cm, age_months) parts.append(f"{height_cm:.1f} cm ({ph['percentile_str']} · WHO Median: {ph['p50']:.1f} cm)") if not parts: return f"📏 Messung ({date_str}) ohne Werte geloggt." details = " · ".join(parts) return f"📏 Wachstum ({date_str}, ~{age_months:.1f} Mon): {details} [Normbereich: P3–P97]"