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