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.
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.
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.
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:
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:
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_versionused 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_REQUIREDegress 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:
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.
Related
- Configuring age and sex specific reference ranges in LIMS — the schema design and governance path for the interval store this node reads.
- Delta Validation & Trend Analysis — the downstream stage that compares in-range results against a patient’s prior values.
- Critical Value Alert Routing — the independent escalation path that a
CRITICAL_*verdict triggers. - Threshold Tuning & Calibration — how the bounds this node enforces are validated and retuned in shadow mode.
- Test Code Taxonomy & Standards — the LOINC identity resolution that keys every interval lookup.
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.