Skip to content

Panel Context

Panel context reduces ordinary laboratory ambiguity without turning a panel order into a row-level LOINC mapping.

Source of truth

The checked-in artifact config/panel_context_index.2.82.json is compiled from reviewed inputs only; nothing in it is typed by hand:

  1. config/lab_panel_default_specimens.csv, the clinician-approved default analytical material for the practice's panel headings (an override; a record without a row derives its default from its parent's LOINC System).
  2. The pinned LOINC release file data/Loinc_2.82/Loinc_2.82/AccessoryFiles/PanelsAndForms/PanelsAndForms.csv, which supplies the official parent panel LOINC codes and their member codes (nested sub-panels are flattened to leaves).
  3. config/panel_context_aliases.2.82.json, the release-specific crosswalk from a heading to its parents and to the practice's order-set tests.
  4. The practice's Elation order-set catalog exported by the main project (lab_order_sets_catalog.latest.json): the heading surfaces its panel orders print (--order-set-catalog).
  5. The active registry snapshot (--registry), which decides between a % member and its mass/substance-fraction twin.
  6. Official vendor result definitions when the main project has retrieved them (--vendor-definitions, the schema in config/vendor_definitions.schema.json): their component names become members through the registry and the release aliases.

The practice's order sets are the primary statement of what will arrive by fax, so every panel-class order the practice places has a record. An entry in the aliases file looks like this:

"Iron, TIBC, and Ferritin Panel": {
  "panel_loinc": "75689-0",
  "additional_parent_loincs": ["50190-8"],
  "aliases": ["iron, tibc and ferritin"],
  "order_set_tests": [
    {"vendor": "911547826422", "code": "5616"},
    {"name": "5616 Iron, TIBC and Ferritin Panel"}
  ]
}
  • panel_loinc plus additional_parent_loincs: members are the union of every parent's leaves. LOINC splits Quest's iron, TIBC and ferritin order across 75689-0 (iron, saturation, ferritin, transferrin in molar properties) and 50190-8 (iron, TIBC, UIBC, saturation in mass properties); the record carries both.
  • Fraction twins: when the union holds two active terms that differ only by MFr versus SFr (a % the unit validator cannot separate), the one the registry targets stays and the other is dropped (14801-5 leaves because Dr Tro approved 2502-3). Mass and molar concentration twins both stay: the unit decides them.
  • order_set_tests: the Elation lab_vendor id plus the vendor test code (the catalog's stable key), or the catalog test name for vendor-less rows. The test's name and its vendor-number-stripped name become aliases; "synonyms": true opts the Elation synonyms in (they can be result names, so they are not automatic). Order-set names never become headings. A test the catalog no longer has is a build error, not a silent miss.
  • The printed order number is a heading surface too (2.82.5): keyed by the practice's vendor name from config/laboratory_vendors.json (Quest:5616, Labcorp:001321), and bare (5616) only when no other test of the practice's catalog, of any vendor, carries that code. A consumer that knows the laboratory should send the keyed form.
  • An entry with no parent is recognized context with a default specimen and no members (match_status: practice_panel_no_members) until its vendor definition arrives; it never borrows a near-by panel. A clinician may list its result codes instead (component_loincs with component_loincs_source; every code must be an active result term, or the build fails). Since 2.82.6 Quest 7020 THYROID PANEL carries T3 uptake, total T4, free T4 index and TSH, and the Cardio IQ advanced lipid panel (Quest 92145) the lipids, apo B, Lp(a), the ion-mobility particle number, small and medium LDL, large HDL, LDL pattern and peak size, hs-CRP and Lp-PLA2 (Dr Tro's decision, delegated 2026-09-24). An entry that names order-set tests and has no members must say why (members_pending, copied into the index record); otherwise the build fails, so a trusted heading without a member list is never a silent gap.
  • A heading surface belongs to one record and is at least three characters; the build fails otherwise, because the main project trusts any alias hit.

The index carries the alias file's version (2.82.11) and the SHA-256 of every input, including the registry version used for the twin rule; all of it is in mapper provenance. The compiler is local and reproducible; it does not scrape LOINC or vendor web pages.

python -m loinc_mapper build-panel-index `
  --panel-defaults config/lab_panel_default_specimens.csv `
  --panels-and-forms data/Loinc_2.82/Loinc_2.82/AccessoryFiles/PanelsAndForms/PanelsAndForms.csv `
  --aliases config/panel_context_aliases.2.82.json `
  --order-set-catalog <faxautomation>/scripts/elation-sandbox/output/lab_order_sets/lab_order_sets_catalog.latest.json `
  --registry config/mapping_registry.review_cases.20260922.json `
  --output config/panel_context_index.2.82.json `
  --release 2.82

Commit the generated index with the reviewed input files. tests/test_panel_index_content.py pins the shipped index: every reviewed heading resolves as a trusted panel, no record that names a parent is memberless, the iron records carry the practice's result codes.

Runtime behavior

For an exact recognized heading, the mapper records panel_context_source=official_panel_heading and confidence 0.98. It checks whether a candidate is an official component of that panel before using the panel evidence.

"Comprehensive Metabolic Panel" + "Glucose"
  -> parent 24323-8 is context only
  -> 2345-7 is a confirmed component candidate
  -> the configured serum/plasma default may be used only because the row has
     no explicit analytical specimen

The parent code is never emitted for Glucose, Sodium, Creatinine, or any other individual row. Parent-panel candidates remain ineligible unless the row itself is explicitly a panel observation.

A trusted panel (official_panel_heading or explicit_panel_loinc, confidence at least 0.95) also supplies candidates, not only evidence: official components that lexical retrieval missed are injected with source panel_component when the row's own tokens score against them (token_match_scores > 0), at most 12 per row, active result-eligible laboratory terms only. A 150-component urinalysis panel has presence terms (Bilirubin, Protein) that the FTS index ranks below its limit; injection puts them in front of the same validator, ranking and unchanged thresholds as every other candidate. A member without lexical support is never injected, and an llm_enum_hint panel injects nothing. stages.panel_component_candidate_count records how many were added.

Gemini Classification Boundary

panel_context_index.2.82.json includes a closed panel_context_enum.values[].value list. The main project can ask Gemini to select one listed value or UNKNOWN, but it must preserve the raw heading. Only a deterministic local alias match may use panel_context_source="official_panel_heading" and confidence 0.98. A heading cut at the page edge still matches deterministically (2026-09-25): a printed heading of eight or more characters that is a prefix of exactly one record's alias and ends inside a word of it (LIPOPROTEIN FRACTIONATION, ION MOBILIT for ... Ion Mobility) resolves to that record with heading_truncated: true in the resolution; a heading that ends on a word boundary (COMPLETE BLOOD COUNT before ... With Differential) may be a complete, different heading and stays unrecognized, as does a prefix two records share (IRON).

A record names the panel the practice actually runs (2026-10-09, 2.82.11): Quest's OmegaCheck is a whole-blood test, and the record had pointed at LOINC's serum/plasma panel 88884-2, so under the trusted heading its members were the serum fractions and its default specimen serum/plasma — three whole-blood rows filed serum codes in the sandbox. The record is LOINC's Blood panel 90918-4, whose members are the Blood fractions the practice approved (90909-3, 90911-9…90917-6) and whose default is whole blood. CPL's 1000 CBC W/AUTO DIFF AND PLATELET names the CBC record by the words after its order number (alias cbc w auto diff and platelet).

A heading is the words the laboratory prints, punctuation aside (2026-10-08, 2.82.10): LabCorp heads its NMR report NMR LipoProfile ® test, which normalize_text reads as nmr lipoprofile test, and the NMR Lipoprotein Panel record carries nmr lipoprofile and nmr lipoprofile test. Until then the page fell to sibling inference, which chose Quest's Cardio IQ record (two shared members beat two of thirty-three) and so lacked the HDL particle term 49748-7 the practice approved; the heading alias names the record outright.

A heading that opens with the laboratory's order number names the record its other words name (2026-10-02, every vendor; PanelContextIndex.resolve, so the main project's heading decision, the mapper and sibling inference agree): CPL prints 1501 URINALYSIS W/REFLEX MICRO, and 2.82.9 carries URINALYSIS W/REFLEX MICRO on Complete Urinalysis With Microscopy. Only a leading run of three or more digits followed by words that name a record by themselves (or as a heading cut at the page edge): 24 HOUR URINE keeps its number, a number alone or 2026 RESULTS names nothing, a number inside a heading stays part of it. The mirror holds at the end (2026-10-06, PanelContextIndex._record_before_trailing_code): LabCorp prints Basic Metabolic Panel (8)-322758 and Comp. Metabolic Panel (14)-322000, and the parenthesised count and the trailing order code are not the panel's name; the words name the record by themselves, a count alone is cut the same way, and an order code the index carries for another record vetoes. When the number is an order code the index already carries for another record (Quest:7655), the heading names nothing here and the order-code route decides as before. A row a caller already labelled unrecognized_panel_heading (read against an earlier index) keeps its old reading: the rule would name a record it cannot trust and switch sibling inference off for the section's rows (a stored CPL page lost its epithelial-cell and bacteria filings that way). The main project asks the index without a label, so new faxes get the reading.

A Gemini-only selection uses panel_context_source="llm_enum_hint" with a maximum confidence of 0.70. The mapper may use it for retrieval/ranking, but it cannot apply a panel default specimen or set a parent LOINC hint. An unknown heading is retained as unrecognized_panel_heading; it is not silently dropped or guessed.

After ordinary unit and six-axis validation, a safe documented component of a trusted official panel receives a strong rank preference over non-member siblings. This resolves a CMP Globulin row to calculated total globulin without treating the parent panel as a hard code filter. Explicit unit, method, time, specimen, property, or scale conflicts still reject that candidate.

The preference needs the row's own words. A confirmed member is lifted only with lexical support: an exact release-name match, a component axis match, a universal component or exact-alias match, or row_name_support, a row word found in the term's analyte words after the term's own axis words are removed. The analyte words are the head of the long common name (before the property bracket, in/of the system or by the method), the component and the head of the consumer name, because LOINC files Specific gravity, Color or Appearance under the placeholder component Observation; the axis words are the rest of the long common name plus the method, system, time, scale and property values (singular and plural fold). A retrieval token score is not support: Automated in Nucleated RBCs, Automated is the method of every CBC member, and a umls_name token score is measured against the concept name, not the row. Related names and short names are not consulted either, because LOINC lists axis synonyms there (Percent, Auto, Bld) beside analyte synonyms. Membership alone never carries a candidate past the thresholds: Nucleated RBC % and Nucleated RBCs, Automated under a CBC heading share no analyte word with the member Other cells/Leukocytes (58409-4), so that member takes the non-member path and the row reaches Erythrocytes.nucleated/Leukocytes (58413-6) through its approved aliases instead. An unsupported sole member also leaves its siblings' scores untouched.

When a confirmed member already answers the row, the other members are context, not competitors. Such an anchored member is a reviewed registry alias or a member whose own release name is the row (exact_component_match); while one is present, the other supported members take the ordinary prior blend instead of the boost, which under the 1.0 cap could only narrow the answer's margin. IRON BINDING CAPACITY under the iron heading is 2500-7's own component, so the boosted Iron and UIBC siblings no longer pull its margin under 0.08. Two exact members remain a real ambiguity: both keep the boost and the row reviews.

A panel is ranking evidence for its members, never against the rest (2026-10-05). The prior blend (0.78 * score + 0.22 * prior) lifts a candidate the record names and leaves every other score as it was; it had run for every candidate under any active panel, so a candidate the record does not list lost 22 % of its score. Two production rows showed it: a laboratory that prints each serology test as its own heading (2739 HEPATITIS B SURF AG, which names no record) saw a hepatitis B surface antigen row that files bare at 0.902 abstain under its heading at 0.791, and under a page-inferred CBC, whose LOINC member list (2.82) predates immature granulocyte and nucleated RBC reporting, the practice's approved immature-granulocyte code led its group at 0.817, under the 0.82 line. The explicit softening of non-members when a supported member competes for the row stands.

An analytical specimen stated on the row always wins. A conflicting explicit row specimen is a safety diagnostic, not something the panel default can erase. collection_specimen is draw provenance: Blood, Venous does not claim that every component was analyzed as whole blood, serum, or plasma, but it rules out physically disjoint urine and CSF candidates.

When several members share one component, system and method and differ only in Scale (the urine test-strip pairs Glucose [Presence] / Glucose [Mass/volume], Ketones, Protein), the practice's own approvals decide the twin: the registry's clinician approvals for terms of that system and method are the practice's decomposition of the panel, and when they all use one Scale (every approved urine strip term is an Ord presence term) the twin with that Scale keeps the member boost while the other takes the non-member path with panel_member_preferred: false and stays listed as an alternative. A member without a twin, or twins for a system and method the practice has approved in mixed Scales or not at all, keep the ordinary boost and a tie still reviews.

A heading never gates

The panel index accelerates mapping; a missing entry ends in a review case, never in a failure. Three rules keep that true:

  • Soft default. A trusted heading whose record has no members yet (a practice panel without a LOINC parent, or one waiting for its vendor definition) lends its clinician default to every candidate as evidence (panel_default_soft), never as a hard fact. A candidate whose System is compatible passes with panel default specimen matches system; an incompatible System is review-blocked (candidate system conflicts with the panel's default specimen; confirm the specimen), so T4 (THYROXINE), TOTAL under Quest's THYROID PANEL keeps the serum term and reviews the blood and dried-blood-spot siblings, while a row whose only candidates conflict abstains for review instead of failing or taking the default's code. PPP and PRP count as plasma Systems, so a coagulation heading with a plasma default accepts PT, INR and aPTT. A soft default never satisfies a registry required_specimen.
  • Inference is not switched off by an unrecognized heading. Rows under a heading the index does not know still take part in sibling inference when they share a report_section_id; the inferred resolution records the heading it replaced (heading_name). A recognized heading, with or without members, is never replaced by inference: PT, INR and aPTT under Coagulation Panel stay coagulation rows even when urine rows share their section. Its rows still count toward the section's size, and a record is inferred only when its members are at least half of the section's rows, so a page-wide section that mixes panels infers none. A row names a member through its exact release aliases and through the practice's approvals: an approval of the row's own surface, or one contained in it whose extra words are its target's release words (panels._row_identity_codes). A CBC trend report printed without a heading (Auto WBC, MCV, MCHC, RDW...) has few release aliases but every row approved, so it infers the CBC record; an unexplained containment (LDL in LDL PATTERN) names nothing.
  • Sibling inference is trusted for ranking. An inferred panel, like a trusted or corroborated heading (panels.ranking_trusted, evidence panel_trusted), gives its confirmed members the boost, member injection and the missing-unit exception; a hard default specimen still needs a trusted heading. The SapBERT reranker's candidate cutoff (50) never drops a confirmed member with lexical support: %MONO's member sat at retrieval position 82.
  • A Gemini hint the rows corroborate is trusted for ranking. An llm_enum_hint (confidence at most 0.70) is advisory on its own, but when at least two rows of the same section resolve through their release aliases to members of the hinted record, the section's resolution becomes corroborated_panel_hint at 0.95: the member boost, member injection and a required_panel scope accept it, a hard default specimen never does. A hint the rows contradict, or a single row, stays a hint. Quest's URINALYSIS REFLEX heading is also an alias of the complete urinalysis record since 2.82.4, so it resolves deterministically.
  • Specific over broad. When two records match the same number of member rows, the record whose members are the larger share of the matches wins (a four-row iron table infers the iron panel, not the anemia evaluation that contains it); an exact tie stays undecided.

Main-project payload

For rows listed under a recognized report heading, send the panel index record key the heading resolved to (every key is an alias of its own record; the printed heading stays in the main project) and its provenance with every row in the report section. The field contract is in OCR recovery:

LabObservation(
    raw_name="Calcium",
    value="8.8",
    unit="mg/dL",
    observation_domain="LAB_RESULT",
    panel_context="Comprehensive Metabolic Panel",
    panel_context_source="official_panel_heading",
    panel_context_confidence=0.98,
    panel_loinc_hint="24323-8",  # when the main project knows it
    report_section_id="p2:COMP. METABOLIC PANEL (14)",
)

Only pass panel_loinc_hint when it came from a trusted local mapping or a known order/result definition. Do not ask OCR or an LLM to invent it. When the hint and the heading name the same record and the heading's source is trusted, the resolution keeps the heading's own source and confidence instead of explicit_panel_loinc 0.98.

What comes back: provenance.stages.panel_context is the resolution, including the record's members (component_codes, sorted), so a consumer reads the list the mapper decided on instead of looking it up again, and panel_name_candidates (additive, 2026-09-25): the records whose members the row's own name names, as [{panel_key, default_specimen}], evidence only and never a hard panel (a printed Glucose names the chemistry and the urinalysis members alike). The mapper reads a row's material from them only when every candidate record shares one default; when they disagree no material is assumed, and when none names the row the default material of the candidate's test family applies (see safety).

Every record of index 2.82.7 carries its members with their release names (members: [{code, display, short}]), so a consumer can ask the index without the catalog: PanelContextIndex.member_codes_for_name(name) (the member codes across every record a printed row name names; with a catalog the grounding rule matches the terms themselves) and panels_for_name(name) (the records, as (panel_key, default_specimen)), and panels.row_member_codes(observation, catalog, registry) (the codes a row names by its release aliases and the practice's approvals). A consumer that resolved a heading through an LLM should count it only when a row under it names one of its members. Membership reads the whole row: every content word of the printed name must be explained by the member's names (with a catalog, the grounding rule plus row_words_unexplained empty), so Ab alone never makes a row a member of every antibody panel. provenance.stages.candidate_decision_trace lists every confirmed member's verdict first (panel_member: true), then the other candidates, 25 entries or all members, so a consumer can tell why an expected member is absent.

If a heading is absent, set one stable report_section_id on rows from the same visual table. The mapper may infer a soft panel context only when at least two independently exact component names point to one unique official panel. A single row never infers a panel, and an explicit heading always wins.

Maintaining the configuration

  • Add a row to lab_panel_default_specimens.csv only after clinician approval.
  • Add an alias in panel_context_aliases.2.82.json only when it maps to the exact official parent panel in the pinned release; add an order-set test by its vendor id and vendor code, never by a hand-typed heading list.
  • When the practice changes its order sets, re-export the catalog in the main project and rebuild; a removed test fails the build so the entry is fixed.
  • Keep complex mixed-material panels conservative. The compiler supports clear component rules such as whole blood for ESR; serum/plasma for CRP; it does not turn whole blood plus serum/plasma into a global specimen assertion.
  • Rebuild the index, bump the alias file's version, and run the unit tests plus runtime canaries after changing any panel input or LOINC release.

Panel configuration is not an alias-to-result registry. To approve a result meaning across laboratories, use the clinician review and immutable registry publication workflow described in Clinician-governed learning.