Functional Weave
Code in TypeScript

health.news2@1.0.1

impl/python.py

4,452 bytes · the Python implementation · view raw

import math

from .health_news2_types import News2Observations, News2Risk, News2Score

_CONSCIOUSNESS = ("alert", "confusion", "voice", "pain", "unresponsive")


def _check_int(name: str, value: object, low: int, high: int) -> None:
    # Ranges are guard rails against unit and typing mistakes, not RCP rules;
    # out of range is an error, never clamped.
    if isinstance(value, bool) or not isinstance(value, int) or value < low or value > high:
        raise ValueError("%s must be a whole number from %d to %d, received %r" % (name, low, high, value))


def _respiration_score(rate: int) -> int:
    if rate <= 8:
        return 3
    if rate <= 11:
        return 1
    if rate <= 20:
        return 0
    if rate <= 24:
        return 2
    return 3


def _spo2_score(spo2: int, scale: int, on_oxygen: bool) -> int:
    # Scale 2 targets 88-92%, so on oxygen a high saturation scores; on air it
    # does not. Scale 1 ignores oxygen here: that is scored separately.
    if scale == 1:
        if spo2 <= 91:
            return 3
        if spo2 <= 93:
            return 2
        if spo2 <= 95:
            return 1
        return 0
    if spo2 <= 83:
        return 3
    if spo2 <= 85:
        return 2
    if spo2 <= 87:
        return 1
    if spo2 <= 92 or not on_oxygen:
        return 0
    if spo2 <= 94:
        return 1
    if spo2 <= 96:
        return 2
    return 3


def _systolic_score(sbp: int) -> int:
    if sbp <= 90:
        return 3
    if sbp <= 100:
        return 2
    if sbp <= 110:
        return 1
    if sbp <= 219:
        return 0
    return 3


def _pulse_score(pulse: int) -> int:
    if pulse <= 40:
        return 3
    if pulse <= 50:
        return 1
    if pulse <= 90:
        return 0
    if pulse <= 110:
        return 1
    if pulse <= 130:
        return 2
    return 3


def _temperature_score(t: float) -> int:
    # Chart rows are to one decimal place; a value between rows scores with
    # the higher row ("at or below").
    if t <= 35.0:
        return 3
    if t <= 36.0:
        return 1
    if t <= 38.0:
        return 0
    if t <= 39.0:
        return 1
    return 2


def news2_score(observations: News2Observations) -> News2Score:
    """The NEWS2 score (Royal College of Physicians, 2017) for one set of adult
    observations: each parameter scored from chart 1, plus 2 for supplemental
    oxygen, summed; and the clinical risk from chart 2, where a single
    parameter scoring 3 raises a low aggregate to low-medium."""
    o = observations
    _check_int("respirationRate", o.respiration_rate, 1, 80)
    _check_int("spo2", o.spo2, 1, 100)
    if isinstance(o.spo2_scale, bool) or o.spo2_scale not in (1, 2) or not isinstance(o.spo2_scale, int):
        raise ValueError("spo2Scale must be 1 or 2, received %r" % (o.spo2_scale,))
    if not isinstance(o.on_oxygen, bool):
        raise ValueError("onOxygen must be true or false, received %r" % (o.on_oxygen,))
    _check_int("systolicBp", o.systolic_bp, 20, 300)
    _check_int("pulse", o.pulse, 1, 300)
    if o.consciousness not in _CONSCIOUSNESS:
        raise ValueError(
            "consciousness must be one of alert, confusion, voice, pain, unresponsive, received %s" % (o.consciousness,)
        )
    t = o.temperature
    if isinstance(t, bool) or not isinstance(t, (int, float)) or not math.isfinite(t) or t < 25 or t > 45:
        raise ValueError("temperature must be from 25.0 to 45.0 degrees Celsius, received %r" % (t,))

    respiration = _respiration_score(o.respiration_rate)
    spo2 = _spo2_score(o.spo2, o.spo2_scale, o.on_oxygen)
    air_or_oxygen = 2 if o.on_oxygen else 0
    systolic = _systolic_score(o.systolic_bp)
    pulse = _pulse_score(o.pulse)
    consciousness = 0 if o.consciousness == "alert" else 3
    temperature = _temperature_score(t)
    total = respiration + spo2 + air_or_oxygen + systolic + pulse + consciousness + temperature
    red = 3 in (respiration, spo2, systolic, pulse, consciousness, temperature)
    risk: News2Risk = "low"
    if total >= 7:
        risk = "high"
    elif total >= 5:
        risk = "medium"
    elif red:
        risk = "low-medium"
    return News2Score(
        total=total,
        respiration_rate=respiration,
        spo2=spo2,
        air_or_oxygen=air_or_oxygen,
        systolic_bp=systolic,
        pulse=pulse,
        consciousness=consciousness,
        temperature=temperature,
        red_score=red,
        clinical_risk=risk,
    )