AXLR / Catalog / health

bmi_bsa

Body-mass index with the WHO adult categories (and a percentile note under 18) and body-surface area by the Mosteller, DuBois, Haycock and Gehan-George formulas, from metric or imperial weight and height. BMI is an exact fraction; every output carries a disclaimer: not medical advice, verify with a clinician.

Install

/tools install bmi_bsa
/tools install bmi_bsa@1.0.0

In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · bmi_bsa 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool bmi_bsa; each run is approved as a local_exec.

Arguments

One JSON object on stdin, validated against the schema before the program starts.

nametyperequireddefaultmeaning
weightnumber | stringyeskg (metric) or lb (imperial)
heightnumber | stringyescm (metric) or in (imperial)
unitsstringno"metric"one of "metric", "imperial"
agenumbernoYears; under 18 the adult categories are withheld. ≥ 0; ≤ 130
Input schema (JSON)
{
  "type": "object",
  "required": [
    "weight",
    "height"
  ],
  "additionalProperties": false,
  "properties": {
    "weight": {
      "type": [
        "number",
        "string"
      ],
      "description": "kg (metric) or lb (imperial)"
    },
    "height": {
      "type": [
        "number",
        "string"
      ],
      "description": "cm (metric) or in (imperial)"
    },
    "units": {
      "type": "string",
      "enum": [
        "metric",
        "imperial"
      ],
      "default": "metric"
    },
    "age": {
      "type": "number",
      "minimum": 0,
      "maximum": 130,
      "description": "Years; under 18 the adult categories are withheld"
    }
  }
}

Example

Input

{
  "weight": 80,
  "height": 180
}

Output (stdout)

{
  "bmi": "24.69",
  "who_category": "Normal range",
  "bsa_m2": {
    "mosteller": "2.0000",
    "dubois": "1.9964"
  }
}

Files

The command the install card registers is python3 bmi_bsa.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.

bmi_bsa.py · sha256 be05a7e8c51c3db93a908b76dfaff774a2f875126772d0ee5a6abab7ca6be28e
#!/usr/bin/env python3
"""Body-mass index with the WHO adult categories and body-surface area by four formulas.

Input (JSON on stdin):
  weight   number or decimal string (kg, or lb with units "imperial")
  height   number or decimal string (cm, or in with units "imperial")
  units    "metric" | "imperial"            optional, default metric
  age      years                            optional; under 18 the WHO adult cut-offs do not apply

BMI = kg / m^2 (exact fraction). BSA in m^2 (H in cm, W in kg):
  Mosteller (1987)      sqrt(H * W / 3600)
  DuBois & DuBois (1916) 0.007184 * W^0.425 * H^0.725
  Haycock (1978)        0.024265 * W^0.5378 * H^0.3964
  Gehan & George (1970) 0.0235 * W^0.51456 * H^0.42246
"""
import json
import math
import re
import sys
from decimal import ROUND_HALF_UP, Decimal, localcontext
from fractions import Fraction

DISCLAIMER = ("Not medical advice: arithmetic from published formulas only. Verify every result with a "
              "qualified clinician before any clinical use.")
LB, INCH = Fraction("0.45359237"), Fraction("2.54")
WHO = [  # (lower bound inclusive, category, sub-category), WHO adult BMI classification
    (Fraction(0), "Underweight", "Severe thinness"),
    (Fraction(16), "Underweight", "Moderate thinness"),
    (Fraction(17), "Underweight", "Mild thinness"),
    (Fraction("18.5"), "Normal range", "Normal range"),
    (Fraction(25), "Pre-obese", "Pre-obese"),
    (Fraction(30), "Obese", "Obese class I"),
    (Fraction(35), "Obese", "Obese class II"),
    (Fraction(40), "Obese", "Obese class III"),
]
DECIMAL = re.compile(r"\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d{1,3})?\s*")


def fail(message):
    print(json.dumps({"error": message, "disclaimer": DISCLAIMER}))
    sys.exit(2)


def number(value, field):
    if isinstance(value, bool) or not isinstance(value, (int, float, str)):
        fail(f"{field} must be a number or a decimal string")
    if isinstance(value, str) and not DECIMAL.fullmatch(value):
        fail(f"{field} must be a number or a decimal string")
    try:
        result = Fraction(str(value).strip())
    except (ValueError, ZeroDivisionError):
        fail(f"{field} must be a number or a decimal string")
    if abs(result) > 10 ** 12:
        fail(f"{field} is too large")
    if result <= 0:
        fail(f"{field} must be positive")
    return result


def fixed(value, places):
    with localcontext() as ctx:
        ctx.prec = 60
        if isinstance(value, Fraction):
            value = Decimal(value.numerator) / Decimal(value.denominator)
        return str(value.quantize(Decimal(1).scaleb(-places), rounding=ROUND_HALF_UP))


def main():
    try:
        request = json.load(sys.stdin)
    except ValueError as error:
        fail(f"stdin is not JSON: {error}")
    if not isinstance(request, dict):
        fail("input must be a JSON object")
    allowed = {"weight", "height", "units", "age"}
    extra = sorted(set(request) - allowed)
    if extra:
        fail(f"unknown field(s): {', '.join(extra)}")
    units = request.get("units", "metric")
    if units not in ("metric", "imperial"):
        fail("units must be metric or imperial")
    weight = number(request.get("weight"), "weight")
    height = number(request.get("height"), "height")
    age = request.get("age")
    if age is not None and (isinstance(age, bool) or not isinstance(age, (int, float)) or age < 0 or age > 130):
        fail("age must be a number of years between 0 and 130")
    kg, cm = (weight, height) if units == "metric" else (weight * LB, height * INCH)
    if not (Fraction(20) <= cm <= Fraction(300)):
        fail("height must be between 20 and 300 cm (8 and 118 in)")
    if not (Fraction(1) <= kg <= Fraction(700)):
        fail("weight must be between 1 and 700 kg")
    bmi = kg / (cm / 100) ** 2
    band = [row for row in WHO if bmi >= row[0]][-1]
    with localcontext() as ctx:
        ctx.prec = 60
        mosteller = (Decimal((cm * kg).numerator) / Decimal((cm * kg).denominator) / 3600).sqrt()
    w, h = float(kg), float(cm)
    bsa = {
        "mosteller": fixed(mosteller, 4),
        "dubois": fixed(Decimal(repr(0.007184 * math.pow(w, 0.425) * math.pow(h, 0.725))), 4),
        "haycock": fixed(Decimal(repr(0.024265 * math.pow(w, 0.5378) * math.pow(h, 0.3964))), 4),
        "gehan_george": fixed(Decimal(repr(0.0235 * math.pow(w, 0.51456) * math.pow(h, 0.42246))), 4),
    }
    notes = []
    if age is not None and age < 18:
        notes.append("Under 18: the WHO adult cut-offs do not apply; use BMI-for-age percentiles "
                     "(WHO growth reference or national charts).")
    result = {
        "weight_kg": fixed(kg, 3),
        "height_cm": fixed(cm, 2),
        "bmi": fixed(bmi, 2),
        "bmi_rounded": fixed(bmi, 1),
        "bmi_exact": str(bmi),
        "who_category": band[1] if age is None or age >= 18 else None,
        "who_subcategory": band[2] if age is None or age >= 18 else None,
        "bsa_m2": bsa,
        "notes": notes,
        "source": "WHO adult BMI classification; Mosteller 1987, DuBois & DuBois 1916, Haycock 1978, Gehan & George 1970",
        "disclaimer": DISCLAIMER,
    }
    print(json.dumps(result, ensure_ascii=False))


if __name__ == "__main__":
    try:
        main()
    except SystemExit:
        raise
    except Exception as error:  # last resort: a usage error, never a traceback
        fail(f"invalid input ({type(error).__name__})")

Where it is served

The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:

curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/bmi_bsa/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/bmi_bsa/1.0.0/bmi_bsa.py