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, )