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:
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).- 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). 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.- 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). - The active registry snapshot (
--registry), which decides between a%member and its mass/substance-fraction twin. - Official vendor result definitions when the main project has retrieved
them (
--vendor-definitions, the schema inconfig/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_loincplusadditional_parent_loincs: members are the union of every parent's leaves. LOINC splits Quest's iron, TIBC and ferritin order across75689-0(iron, saturation, ferritin, transferrin in molar properties) and50190-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
MFrversusSFr(a%the unit validator cannot separate), the one the registry targets stays and the other is dropped (14801-5leaves because Dr Tro approved2502-3). Mass and molar concentration twins both stay: the unit decides them. order_set_tests: the Elationlab_vendorid 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": trueopts 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_loincswithcomponent_loincs_source; every code must be an active result term, or the build fails). Since 2.82.6 Quest 7020THYROID PANELcarries 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 withpanel default specimen matches system; an incompatible System is review-blocked (candidate system conflicts with the panel's default specimen; confirm the specimen), soT4 (THYROXINE), TOTALunder Quest'sTHYROID PANELkeeps 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.PPPandPRPcount as plasma Systems, so a coagulation heading with a plasma default accepts PT, INR and aPTT. A soft default never satisfies a registryrequired_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 underCoagulation Panelstay 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 (LDLinLDL PATTERN) names nothing. - Sibling inference is trusted for ranking. An inferred panel, like a
trusted or corroborated heading (
panels.ranking_trusted, evidencepanel_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 becomescorroborated_panel_hintat 0.95: the member boost, member injection and arequired_panelscope accept it, a hard default specimen never does. A hint the rows contradict, or a single row, stays a hint. Quest'sURINALYSIS REFLEXheading 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.csvonly after clinician approval. - Add an alias in
panel_context_aliases.2.82.jsononly 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 turnwhole blood plus serum/plasmainto 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.