Reference Range Check Implementation

The reference range check is the deterministic classification stage that turns a normalized numeric result into a bounded verdict — NORMAL, LOW, HIGH, CRITICAL_LOW, or CRITICAL_HIGH — against the single interval that applies to this patient, this analyte, this specimen, and this method. It is not a threshold if statement bolted onto the LIMS; it is a stateless, versioned evaluation node whose every output must be reconstructable from immutable inputs years after release. This page specifies the stage contract, the interval resolution model, the Python patterns that keep it fast and auditable, and the tests that prove it behaves deterministically at the boundaries where patient safety is decided.

Context and Pipeline Position

Within the Clinical Result Validation & Rule Engine Architecture, the reference range check sits immediately after canonicalization and before longitudinal logic. Upstream, the Instrument Data Ingestion & HL7/CSV Pipelines tier delivers a parsed result whose units have been harmonized to the Unified Code for Units of Measure (UCUM) and whose analyte identity is resolved to LOINC through the laboratory’s Test Code Taxonomy & Standards. The reference range node consumes that canonical result, attaches a range-based verdict, and hands off: normal and out-of-range values flow into Delta Validation & Trend Analysis, while values that breach a critical bound are simultaneously escalated through Critical Value Alert Routing. The decision boundaries this node enforces are themselves owned and retuned by Threshold Tuning & Calibration, so the range check reads its intervals from configuration rather than hard-coding them.

Pipeline position of the reference range check A canonical result flows from instrument ingestion through canonicalization into the reference range check, the highlighted stage. The range check reads its intervals from a versioned, effective-dated interval configuration store and forks its classified output to delta validation for in-range trend analysis and to critical value routing when a panic bound is breached. Ingestion HL7 / CSV parse Canonicalization UCUM · LOINC Reference Range Check stateless · pure function NORMAL · LOW · HIGH · CRIT Delta Validation in-range trend analysis Critical Value Routing panic-bound escalation Interval Config versioned · effective-dated

The node is stateless with respect to patient history — it never queries prior results. That separation is deliberate: it keeps range classification a pure function of the current result plus the resolved interval, which is what makes it cheap to parallelize and trivial to replay during an audit.

Stage Boundaries

The reference range check exposes one ingress contract and two egress contracts, with explicit failure semantics at each edge. Treat these as the only supported ways data enters or leaves the node.

Ingress — the canonical result. The node accepts a fully normalized result: a numeric value, a UCUM unit, a resolved loinc code, a specimen matrix, an instrument_method identifier, and a demographic block (birth_date, sex, plus optional pregnancy_status and fasting_state). Any payload that fails schema validation is rejected at the boundary, never partially processed — this is the same discipline enforced by the upstream schema validation and error-handling layer, applied again locally because the range node must not trust that an upstream contract was honored.

Egress A — the classified result. On success the node emits the original result plus a RangeVerdict: the resolved interval (lower bound, upper bound, critical bounds), the interval’s version identifier, the classification flag, and the demographic context that selected the interval. This is an additive transformation; the input is never mutated in place.

Egress B — the review hold. When an interval cannot be resolved unambiguously — missing demographics, an analyte with no configured interval for the specimen, or overlapping intervals with no priority winner — the node does not guess. It emits the result with a REVIEW_REQUIRED flag and a machine-readable reason code, degrading toward safety so a human technologist adjudicates rather than an auto-verified false NORMAL reaching the chart.

Failure semantics. A transport or infrastructure failure (config store unreachable, deserialization error) is retryable and must never be silently swallowed into a NORMAL; the result is quarantined and re-driven. A semantic failure (no applicable interval) is not retryable — retrying produces the same ambiguity — so it routes straight to review. The invariant across every edge: the absence of a positive, resolvable interval never resolves to auto-release.

Interval Resolution Specification

Laboratories maintain overlapping intervals stratified along several demographic and preanalytical axes. Resolution must select exactly one interval deterministically. The table below is the field contract for a single interval row; store these in a version-controlled configuration store with effective dating, and require dual approval for any clinical edit — the governance path defined in Configuring age and sex specific reference ranges in LIMS.

Field Type Semantics
loinc string Resolved analyte identity; the primary partition key.
specimen enum Serum, plasma, whole blood, urine, CSF — intervals are matrix-specific.
method_version string Instrument/method identifier; prevents cross-method interval drift.
age_low_days / age_high_days int Half-open cohort boundary [low, high) in whole days.
sex enum male, female, any; unknown never silently maps to a sex-specific row.
condition enum none, pregnant, fasting — preanalytical qualifier.
lower / upper Decimal Reference interval bounds in the canonical UCUM unit.
critical_low / critical_high Decimal? Panic bounds; null where the analyte has no critical on that side.
priority int Tie-break weight; higher wins when multiple rows match.
effective_from / effective_to date Version window used at replay time to select the historical row.

Resolution proceeds as a state machine: filter candidate rows by loinc + specimen + method_version, intersect with the demographic cohort (age in days at collection, sex, condition), restrict to rows whose effective window contains the collection date, then break any remaining tie by descending priority. Zero survivors routes to review; more than one survivor after the priority sort is a configuration defect and also routes to review while emitting an alarm.

Interval resolution state machine Candidate interval rows are filtered by analyte, then by demographic cohort, then by effective date, then sorted by descending priority. Exactly one survivor resolves the interval and classifies the value; zero survivors routes to a review hold; more than one survivor at the top priority raises a configuration alarm and also routes to review. candidate interval rows Filter by Analyte loinc · specimen · method_version Filter by Cohort age_days · sex · condition Filter by Effective Date window contains collection date Priority Sort descending priority · break ties = 1 survivor 0 survivors tie at top Resolved one interval → classify value Review Hold REVIEW_REQUIRED Config Alarm + Review Hold priority tie is a defect

Age must be computed in whole days from birth_date to the specimen collection_datetime, timezone-aware, so a neonate on day-of-life 3 never collides with a pediatric cohort boundary. Half-open boundaries [age_low_days, age_high_days) eliminate the off-by-one where an infant at exactly the boundary day matches two cohorts.

Implementation Patterns

Model the contracts with Pydantic v2 so the ingress schema is enforced by construction and the verdict is serializable for the audit sink. The classification itself is a pure function; the resolver is the only component that touches the configuration store.

python
from __future__ import annotations

from datetime import date, datetime, timezone
from decimal import Decimal
from enum import Enum

from pydantic import BaseModel, ConfigDict, Field


class Flag(str, Enum):
    NORMAL = "NORMAL"
    LOW = "LOW"
    HIGH = "HIGH"
    CRITICAL_LOW = "CRITICAL_LOW"
    CRITICAL_HIGH = "CRITICAL_HIGH"
    REVIEW_REQUIRED = "REVIEW_REQUIRED"


class Sex(str, Enum):
    MALE = "male"
    FEMALE = "female"
    UNKNOWN = "unknown"


class CanonicalResult(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    loinc: str
    value: Decimal
    unit: str  # UCUM
    specimen: str
    instrument_method: str
    birth_date: date
    sex: Sex
    condition: str = "none"
    collection_datetime: datetime


class Interval(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    version: str
    lower: Decimal
    upper: Decimal
    critical_low: Decimal | None = None
    critical_high: Decimal | None = None
    priority: int = 0


class RangeVerdict(BaseModel):
    flag: Flag
    interval_version: str | None = None
    lower: Decimal | None = None
    upper: Decimal | None = None
    reason: str | None = None
    cohort_age_days: int = Field(ge=0)

Age resolution and classification are deterministic and side-effect free, which is what makes the node replayable:

python
def age_in_days(birth: date, collected: datetime) -> int:
    if collected.tzinfo is None:
        raise ValueError("collection_datetime must be timezone-aware")
    collected_utc = collected.astimezone(timezone.utc).date()
    return (collected_utc - birth).days


def classify(value: Decimal, interval: Interval) -> Flag:
    if interval.critical_low is not None and value <= interval.critical_low:
        return Flag.CRITICAL_LOW
    if interval.critical_high is not None and value >= interval.critical_high:
        return Flag.CRITICAL_HIGH
    if value < interval.lower:
        return Flag.LOW
    if value > interval.upper:
        return Flag.HIGH
    return Flag.NORMAL

Note the ordering: critical bounds are tested before the ordinary out-of-range bounds so a value that is both HIGH and CRITICAL_HIGH classifies as critical. Comparisons use Decimal end to end; a float interval store would introduce representation error at exactly the boundary values where the classification changes.

The resolver isolates I/O. In a high-volume operation it fronts the configuration store with an in-process cache keyed by (loinc, specimen, method_version) and refreshed on version change, so steady-state classification touches no network:

python
import asyncio


class IntervalResolver:
    def __init__(self, store: "IntervalStore") -> None:
        self._store = store
        self._sem = asyncio.Semaphore(64)

    async def resolve(self, r: CanonicalResult) -> Interval | None:
        age_days = age_in_days(r.birth_date, r.collection_datetime)
        async with self._sem:
            candidates = await self._store.candidates(
                loinc=r.loinc,
                specimen=r.specimen,
                method_version=r.instrument_method,
                as_of=r.collection_datetime.date(),
            )
        matched = [
            c for c in candidates
            if c.age_low_days <= age_days < c.age_high_days
            and c.sex in (r.sex.value, "any")
            and c.condition in (r.condition, "none")
        ]
        if not matched:
            return None
        matched.sort(key=lambda c: c.priority, reverse=True)
        if len(matched) > 1 and matched[0].priority == matched[1].priority:
            return None  # ambiguous → review, alarm raised by caller
        return matched[0].interval


async def evaluate(
    result: CanonicalResult, resolver: IntervalResolver
) -> RangeVerdict:
    age_days = age_in_days(result.birth_date, result.collection_datetime)
    interval = await resolver.resolve(result)
    if interval is None:
        return RangeVerdict(
            flag=Flag.REVIEW_REQUIRED,
            reason="no_unambiguous_interval",
            cohort_age_days=age_days,
        )
    return RangeVerdict(
        flag=classify(result.value, interval),
        interval_version=interval.version,
        lower=interval.lower,
        upper=interval.upper,
        cohort_age_days=age_days,
    )

A batch of results fans out across the event loop with asyncio.gather, bounded by the resolver’s semaphore so a burst from instrument polling cannot exhaust connections or starve the async batch processing workers upstream. Because classify is pure, throughput scales with resolver cache hit rate, not CPU.

Error Classification and Handling

Errors are tiered so each class has one correct disposition. Collapsing them — treating a config outage the same as a missing interval — is how a laboratory ends up auto-verifying against a stale or absent range.

Tier Example Disposition Retryable
Transport Config store timeout, cache miss + network error Quarantine, re-drive with backoff Yes
Schema Non-UCUM unit, missing birth_date, unparseable value Reject at ingress, route to fix queue No
Semantic No interval for cohort, ambiguous priority tie REVIEW_REQUIRED, human adjudication No

Transport failures never degrade to a verdict; they raise, the orchestrator quarantines the result, and a bounded exponential backoff re-drives it. Schema failures are caught by Pydantic construction — an extra="forbid", frozen=True model turns a malformed payload into a ValidationError at the boundary rather than a silent coercion downstream. Semantic failures are the designed-for path to the review hold: they carry a stable reason code (no_unambiguous_interval, analyte_not_configured, priority_tie) so dashboards can distinguish a genuine coverage gap from an operational blip and feed it back to the interval owners.

The controlling invariant is the safety default: any unresolved state produces a review hold, never an auto-verified NORMAL. A missing interval is a coverage gap to be closed by configuration, not a value to be waved through.

Regulatory Touchpoints

The reference range check is a defined analytic-decision point under several accreditation regimes, and each one constrains the implementation above.

  • CLIA §493.1291© requires reference intervals to appear with reported results; the resolver must persist the exact interval_version used so the released report and the audit record cite the same bounds. This is also why intervals are effective-dated — a result replayed for an inspector must resolve the interval that was live at collection time, not today’s.
  • CLIA §493.1282 / §493.1253 (verification and calibration of test systems) is why intervals are keyed by method_version: an interval validated for one instrument method cannot silently apply to another after a method change.
  • CAP GEN.41350 / automated-verification checklist items require that auto-verification rules be documented, version-controlled, and that failures route to human review — satisfied by the REVIEW_REQUIRED egress and the dual-approval governance on interval edits, which build on the CLIA/CAP Data Boundaries established at the platform layer.
  • 21 CFR Part 11 §11.10 demands attributable, tamper-evident records of the decision; every verdict — including the demographic context and interval version that produced it — is written to an append-only sink with a monotonic sequence number.
  • HIPAA §164.312(b) audit controls apply because the record carries PHI (demographics, analyte, value); access to the interval store and audit sink is role-based and logged.

Because these clauses attach to the decision, the audit event must be emitted before the verdict is acted on, and it must include enough to reconstruct the classification byte-for-byte: raw input, resolved interval version, computed cohort age, and final flag.

Testing and Validation

Deterministic boundary behavior is only credible if it is proven at the boundaries. Combine property-based tests, golden-file fixtures, and a config contract test.

Property-based tests with hypothesis assert the invariants that must hold for every input — most importantly that classification is monotone and that the critical/ordinary ordering never inverts:

python
from decimal import Decimal

from hypothesis import given, strategies as st

from rangecheck import Flag, Interval, classify

decimals = st.decimals(
    min_value=-10_000, max_value=10_000, allow_nan=False, allow_infinity=False
)


@given(value=decimals)
def test_value_inside_bounds_is_normal(value: Decimal) -> None:
    iv = Interval(version="v1", lower=Decimal("1"), upper=Decimal("100"))
    flag = classify(value, iv)
    if Decimal("1") <= value <= Decimal("100"):
        assert flag == Flag.NORMAL
    elif value < Decimal("1"):
        assert flag == Flag.LOW
    else:
        assert flag == Flag.HIGH


@given(value=decimals)
def test_critical_dominates_ordinary(value: Decimal) -> None:
    iv = Interval(
        version="v1",
        lower=Decimal("1"),
        upper=Decimal("100"),
        critical_high=Decimal("50"),
    )
    if value >= Decimal("50"):
        assert classify(value, iv) == Flag.CRITICAL_HIGH

Golden-file fixtures pin real analyte scenarios — a day-of-life-2 neonatal bilirubin, a pregnant-trimester TSH, a fasting-required glucose — as input/expected-verdict pairs so a change in resolution logic surfaces as a diff in review. A contract test loads the production interval configuration and asserts coverage invariants without touching live results: no cohort gaps for configured analytes, no equal-priority overlaps (the ambiguity that forces review), and every critical_low below its lower, every critical_high above its upper. Run it in CI on every interval edit so a bad configuration is caught before it can send a run of results to review.

Round-trip the Pydantic models through model_dump_json() / model_validate_json() in tests to guarantee the verdict written to the audit sink deserializes back identically — the property an inspector relies on years later.

Part of: Clinical Result Validation & Rule Engine Architecture.

Frequently Asked Questions

Why does the reference range check never query the patient's prior results?

Keeping it stateless makes classification a pure function of the current result and the resolved interval, which is what makes the node cheap to parallelize and trivial to replay during an audit. Longitudinal comparison is a separate concern owned by Delta Validation & Trend Analysis.

What happens when no interval matches the patient's demographics?

The node emits a REVIEW_REQUIRED verdict with a machine-readable reason code and routes the result to human adjudication. It never guesses a default interval or auto-verifies a NORMAL; an unresolved state always degrades toward safety.

Why store interval bounds as Decimal instead of float?

Floating-point representation error is largest at exactly the boundary values where the classification flag changes. Using Decimal end to end guarantees that a value equal to a bound classifies identically on every platform and on every replay.

How is a decision reproduced for an inspector years later?

Intervals are effective-dated and versioned, and every verdict is written to an append-only audit sink with the resolved interval_version, the computed cohort age, the raw input, and the final flag. Replaying the input against the interval live at collection time reproduces the classification byte-for-byte.