criteria_matching_types
Shared, typed structures — the canonical output of criteria_matching.
Producer (criteria_matching) emits one CriteriaEvaluation per evaluated row
(a scan, or a patient on the EHR grain), pairing the row's SubjectIdentity
with its per-criterion CriterionOutcomes. Every consumer (CSV tabulation,
scan/EHR eligibility, PDF) reads these instead of re-parsing stringly-typed
DataFrame columns or inverting the config->filter map. This module depends on no
other steps/ module, so producer and consumers can share it freely.
Module
Functions
attach_criteria_tree_verdict
def attach_criteria_tree_verdict( evaluations: list[CriteriaEvaluation], criteria_tree: CriterionTreeNode | None,) ‑> None:Fold a criteria_tree's verdict into each evaluation, in place.
For each evaluation:
- Moves every outcome whose
tree_node_idis set out ofoutcomesinto a leaf-id-to-outcome map. - Evaluates
criteria_treeagainst that map viaevaluate_criterion_tree, which builds one outcome per node (leaf and combinator alike, stamping each one's resolvedtree_groupalong the way) and collectsreview_flagsin the same pass. - Stores every node's outcome on
tree_node_outcomes. A copy of the root's outcome — withconfig_field/output_field/column_nameset to the fixedCRITERIA_TREE_FIELDandtree_node_idcleared, same as every prior phase's hand-built synthetic outcome — is appended back intooutcomes.
The copy, not the same object, is what goes back into outcomes: the
root's real tree_node_id/output_field/column_name (its own node
address, f"criteria_tree.{id}" for a combinator, or a real leaf's own
identity when the whole tree is a single leaf) must survive unchanged in
tree_node_outcomes for per-node introspection, while outcomes still
needs exactly the fixed CRITERIA_TREE_FIELD tag: tabulate_criteria_outcomes
keys off that constant (via column_name) to recognise the tree's
synthetic combined outcome and exclude it from FILTER_CONTEXT_COLUMN,
and attach_criteria_tree_verdict itself keys off tree_node_id is not None to recognise a tree outcome and move it out of outcomes — the
copy must clear it, or a second pass over the same evaluations would
mistake this already-attached root for a leaf to move again.
The root's outcome — and every combinator's — has grain/provenance
derived from its children by _combinator_outcome, not fixed: a tree of
only EHR-provenance, patient-grain leaves now correctly derives
grain=PATIENT, provenance=EHR and routes through build_patient_level_evidence/
patient_grain_status accordingly, rather than being misclassified as
scan data.
A no-op when criteria_tree is None.
Arguments
evaluations: The evaluations to update in place.criteria_tree: The configured tree, orNone.
evaluate_criterion_tree
def evaluate_criterion_tree( node: CriterionTreeNode, leaf_outcomes: Mapping[str, CriterionOutcome], node_outcomes: list[CriterionOutcome], node_groups: Mapping[str, str | None] | None = None, review_flags: list[str] | None = None, rank_context: tuple[int, int] | None = None,) ‑> CriterionOutcome:Recursively reduce one criteria_tree node into its outcome and score.
Dispatches by isinstance rather than by comparing node.node; each group
class declares only its own valid fields. A leaf's outcome is looked up
directly from leaf_outcomes (built by the filter pipeline, real
grain/provenance already stamped). A combinator's outcome is
synthesised by _combinator_outcome. Every node's outcome — leaf and
combinator alike — is appended to node_outcomes before this call
returns, so a caller can inspect every node's and every group's own state
and source, not only the root's.
Arguments
node: The (sub)tree to evaluate.leaf_outcomes: Each leaf id's already-builtCriterionOutcome(from the filter pipeline). Everycriteria_treeleaf always produces a filter, and so an outcome, even when it degrades to anunknownone — a leaf id absent here is a caller error, and raisesKeyErrorrather than degrading silently.node_outcomes: Every node's own outcome, appended here in post-order (children before parents, root last) as this call recurses.node_groups: Each node id's resolved (inherited)group, fromiter_tree_nodes_with_resolved_group.None(the default) leaves every node'stree_groupunset — the shape most tests of the combining rule itself need, without a caller building a group map that plays no part in the state/grain/provenance being checked.review_flags: When passed, any node (leaf or group) whose own state resolves toPASSand whosereview_reasonis set has that reason appended, collected in the same walk as the boolean reduction.None(the default) skips collection entirely.rank_context:(rank, group_size)if this node was assigned a rank by its parent, elseNone. Callers should not pass this; it is set on the recursive calls this function makes into its own children.
Returns
The node's own outcome (its combined state and resolved score, plus
derived grain/provenance for a combinator).
iter_tree_leaves
def iter_tree_leaves( node: CriterionTreeNode,) ‑> collections.abc.Iterator[ColumnCriterion | CodeCriterion | ObservationCriterion | MedicationCriterion | AllergyCriterion | DeviceCriterion | AppointmentWindowCriterion]:Depth-first leaves of a criteria_tree node.
Arguments
node: The (sub)tree to walk.
Returns
An iterator over every leaf (CriterionLeaf) reachable from node.
iter_tree_nodes
def iter_tree_nodes( node: CriterionTreeNode,) ‑> collections.abc.Iterator[ColumnCriterion | CodeCriterion | ObservationCriterion | MedicationCriterion | AllergyCriterion | DeviceCriterion | AppointmentWindowCriterion | AndGroup | OrGroup | AtLeastGroup | NotGroup]:Depth-first every node (leaf or combinator) of a criteria_tree.
NotGroup walks its single child; AndGroup/OrGroup/AtLeastGroup walk
their children list.
Arguments
node: The (sub)tree to walk.
Returns
An iterator over node itself and every node reachable from it.
iter_tree_nodes_with_resolved_group
def iter_tree_nodes_with_resolved_group( node: CriterionTreeNode, inherited_group: str | None = None,) ‑> collections.abc.Iterator[tuple[ColumnCriterion | CodeCriterion | ObservationCriterion | MedicationCriterion | AllergyCriterion | DeviceCriterion | AppointmentWindowCriterion | AndGroup | OrGroup | AtLeastGroup | NotGroup, str | None]]:Depth-first (node, resolved group) pairs, resolving group by inheritance.
Every node — leaf or combinator — is yielded, unlike iter_tree_nodes'
plain walk: a combinator's own outcome needs its resolved group exactly
as a leaf's does, so both are resolved by the same walk here rather than
a leaf-only one plus a second pass for combinators.
A node's resolved group is its own group when set, else the nearest
ancestor's resolved group, else None when no ancestor sets one either.
This is a pure function of the tree's own group fields: no node stores
a parent reference, and nothing here mutates the tree.
CriteriaMatchConfig's validator (_check_group_inheritance) separately
guarantees no node overrides an ancestor's group with a different one,
so in practice every descendant under one ancestor's group always
resolves to that same value.
Arguments
node: The (sub)tree to walk.inherited_group: The resolved group inherited from ancestors already visited. Callers should not pass this; it is set on the recursive calls this function makes into its own children.
Returns
An iterator over (node, resolved_group) for node and every node
reachable from it.
match_strategy_for
def match_strategy_for( dtype: str, op: str, has_glob: bool = False,) ‑> MatchStrategy:Which MatchStrategy handles a legal (dtype, op) pair.
Returns COLUMN (a vectorized ColumnFilter: numeric/bool comparisons,
numeric membership via .isin, and *-less str equality/membership),
SCALAR_MATCH (a per-row str glob MethodFilter), or COLLECTION
(collection-cell membership via make_collection_membership_filter). A
str column routes to COLUMN unless the value carries a * glob
(has_glob), in which case
the per-row matcher is required. Assumes the pair is already known legal
(see ALLOWED_OPS_BY_DTYPE in bitfount.steps.types.operators).
Arguments
dtype: The column's catalogued dtype tag.op: The criterion operator.has_glob: Whether any criterion value contains a*wildcard (only consulted forstrcolumns).
Returns The matching strategy for the pair.
tree_depth
def tree_depth(node: CriterionTreeNode) ‑> int:Longest path from node to any leaf, counting the leaf itself as 1.
Arguments
node: The (sub)tree to measure.
Returns The tree's depth, at least 1.
Classes
AllergyCriterion
class AllergyCriterion(**data: Any):An EHR allergy/intolerance, matched by code, reaction substance, or text.
Per FHIR R4's AllergyIntolerance
(https://hl7.org/fhir/R4/allergyintolerance.html), a qualifying code can
live in either the resource's own top-level code (a CodeableConcept,
0..1 — a resource may carry no coded diagnosis at all) or any of its
reaction[].substance entries (also a CodeableConcept, 0..*). An
entry qualifies against code/code_system if either location
matches — see _build_allergy_filter, which implements this OR across
both locations.
text matches the entry's own code_text instead, for allergens that
arrive with no code at all. That is not a rare edge case: NextGen
resolves an allergen's vocabulary from its allergyTypeId rather than
from what the allergen is, so a food recorded as a Base Ingredient is
pointed at RxNorm, finds no concept there, and arrives uncoded — a
wheat-flour allergy no coded criterion can ever match.
code is optional here (unlike every other code-addressed leaf) so that
a text-only criterion is expressible; supplying neither is rejected.
When both are given they are additional restrictions, not alternatives:
an entry must match both its code and its text to qualify — text
narrows a code match rather than offering an alternative route around
it (unlike the OR the two code locations use against each other). To
match the same conceptual allergen across a coded-vs-uncoded record
split, use two leaves under an OrGroup instead of combining code and
text on one.
Arguments
node: Discriminator identifying this as anallergy-typecriteria_treenode.code: As_BareCodeAddressedLeaf.code; omit it for a text-only criterion.text: Free text matched against the entry'scode_text, glob-capable the same waycodeis (e.g."*PISTACHIO*"), or a list of alternatives. Matched case-insensitively, unlikecode: the same allergen is described in different casing by different backends (NextGen upper-cases descriptions, a FHIR server carries whatever its source system holds), so a case-sensitive match would silently depend on which EHR answered. Searched across every field that names the allergen:code_text, eachcodings[].display, and each reaction'ssubstance_text/substance_codings[].display— see_allergy_text_candidates. A NextGen-sourced entry carries no reactions at all (its reaction records name the symptom, not the allergen, so its querier drops them), so in practice only the first two are populated there.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
criterion_noun : ClassVar[str]
- static
model_config
- static
node : Literal['allergy']
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
AndGroup
class AndGroup(**data: Any):A criteria_tree node requiring every child to pass.
Arguments
children: The child nodes; at least one required.review_reason: SeeColumnCriterion.review_reason. Every child is necessary for anAndGroupto pass, so a flagged child that passes is always part of what produced the result.combine: How this node's ownscorecombines its children's scores when every child has passed (seeevaluate_criterion_tree)."min"(default) takes the weakest child."mean"blends children instead, weighted by each child's ownweight.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
AppointmentCountBound
class AppointmentCountBound(**data: Any):A minimum-count requirement on an AppointmentWindowCriterion's entries.
Arguments
count: The minimum number of qualifying appointments/encounters required. Withperiodunset, this is a plain total over the wholedate_rangewindow. Withperiodset, this is the minimum required in every whole bucket of that period within the window (seeAppointmentWindowCriterion.minimum's own docstring for exactly how buckets are aligned and counted).period: The bucket granularitycountis measured over —"d"/"w"/"m"/"y"(day/week/month/year) — orNonefor a plain total across the whole window, not bucketed at all.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
count : int
- static
model_config
- static
period : Optional[Literal['d', 'w', 'm', 'y']]
AppointmentHistoryCriterion
class AppointmentHistoryCriterion(**data: Any):The compound appointment-history criterion.
A patient qualifies when their appointment/encounter history spans at least
years years and every one of those years holds at least
min_per_year appointments. The two bounds are one criterion, not two:
a length with no density, or a density with no length, does not describe
anything checkable, so a half-specified criterion is rejected.
Both fields None means the criterion is not configured. That spelling is
load-bearing: a task template renders this mapping unconditionally, so two
blank modeller variables arrive here as {years: None, min_per_year: None}
rather than as an absent key.
Arguments
years: Required length of history, in years.min_per_year: Minimum appointments required in each year of the window.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
min_per_year : int | None
- static
model_config
- static
years : int | None
is_configured : bool- Whether both bounds are set, so a filter should be emitted.
AppointmentWindowCriterion
class AppointmentWindowCriterion(**data: Any):A patient's appointment/encounter history: presence, count, or density.
Reads the combined appointment+encounter history
(PREV_APPOINTMENTS_COL/PREV_ENCOUNTERS_COL). A FAIL drawn from
history that was never fetched, or only partly read, reports UNKNOWN
instead — the missing records could have satisfied the criterion.
Arguments
-
node: Discriminator identifying this as anappointment-typecriteria_treenode. -
date_range: The window appointments/encounters are counted within. -
Required: a window-less appointment criterion has nothing to count over. -
bounds: The requirement placed on entries falling withindate_range: -
None(default) — a bare presence check: at least one appointment/encounter falls withindate_range. -
AppointmentCountBound(count=N, period=None)— at leastNappointments/encounters across the wholedate_rangewindow, not bucketed. -
AppointmentCountBound(count=N, period="y"/"m"/"w"/"d")— every whole bucket of that period withindate_rangemust hold at leastNentries. Buckets are anchored atdate_range's own resolved end when set, else the match run's reference date ("today").
A bucketed bounds requires date_range.start to resolve to a concrete date — an open-ended date_range (no start) is only valid with bounds=None or bounds.period is None.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
bounds : AppointmentCountBound | None
- static
date_range : DateWindow
- static
model_config
- static
node : Literal['appointment']
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
AtLeastGroup
class AtLeastGroup(**data: Any):A criteria_tree node requiring at least min_count children to pass.
Arguments
min_count: How many children must pass; must not exceed the number of children.children: The child nodes; at least one required.review_reason: SeeColumnCriterion.review_reason.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
CodeCriterion
class CodeCriterion(**data: Any):A condition or procedure, matched by code or text, presence/date/count.
Arguments
resource: Which resourcecodeandtextare matched against.node: Discriminator identifying this as acode-typecriteria_treenode.code: As_BareCodeAddressedLeaf.code; omit it for a text-only criterion.text: As_TextAddressedLeaf.text, searched across the entry'scode_textand everycodings[].display. A condition or procedure is not always coded — a NextGen problem recorded as free text carries a description and no code — so the name is then the only thing left to match on.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
model_config
- static
node : Literal['code']
- static
resource : Literal['condition', 'procedure']
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
CodeLaterality
class CodeLaterality(**data: Any):Scopes a code-addressed leaf's match to one side, and how to treat unknowns.
Arguments
side: Which side of a paired structure, relative to the row's ownSubjectIdentity.laterality—"index"(the study side),"contralateral"(the opposite side), or"either"(no filtering; the default).is_unknown_ehr_resource_laterality_eligible: Whether an entry qualifies againstsidewhen its own recorded side cannot be inferred — as opposed to the row's own scan laterality being unresolved, whichfail_on_unknown_scan_lateralitygoverns instead (see "Matching laterality" in the design notes).False(the default) means such an entry does not qualify, the conservative choice.fail_on_unknown_scan_laterality: The verdict for the whole leaf when the row's own scan laterality cannot be resolved at all, sosidecannot be evaluated against anything.False(the default) resolves tounknown, genuinely not knowing;Trueresolves to a definite fail instead, letting a sibling any-eye leaf in anOrGroupdecide the group's outcome rather than the unresolved side dragging the whole group tounknown. Both flags are only valid whensideis not"either"— with"either"neither the entry-level nor the scan-level unresolved case can ever arise.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
fail_on_unknown_scan_laterality : bool
- static
is_unknown_ehr_resource_laterality_eligible : bool
- static
model_config
- static
side : Literal['index', 'contralateral', 'either']
CodeMatch
class CodeMatch(*args, **kwargs):One code that qualified, and how it qualified.
Attributes
code: The code as the EHR recorded it, not the configured pattern it matched — a configuredH35.3*reports theH35.31actually present.text: The entry's name for display: itscode_text, else the matched coding'sdisplay, else any coding'sdisplay;Nonewhen the entry carries none. Absent from rows written before it existed.scope:any_eyefor a code qualifying regardless of laterality,study_eyefor one whose inferred laterality had to include the scan eye (and did),fellow_eyefor one whose inferred laterality had to include the other eye (and did).
Variables
- static
code : str
- static
scope : Literal['any_eye', 'study_eye', 'fellow_eye']
- static
text : str | None
CodeMatchEvidence
class CodeMatchEvidence(*args, **kwargs):Which EHR codes drove a code-list criterion's verdict, structurally.
The typed counterpart to a code filter's prose context string. The filters already know the codes; this carries them in a form a consumer can render or filter on without parsing English, and it is persisted inside the criterion's evidence so the patient API can serve it.
A TypedDict rather than a model for the same reason as CriterionEvidence
(which nests it): the value is persisted as JSON, so it must stay a plain dict
through json_safe and json.dumps. total=False because a row written
before this existed carries no key at all — read every key with .get.
Only the codes that decided something appear. A configured code that simply did not match is absent; so is a code matched inside a recency window whose event date proved too old, and a study-eye code whose laterality was inferred and disagreed with the scan eye. All three contributed nothing to the verdict.
Attributes
matched_codes: Every code that qualified, ascending by code. For an inclusion criterion these are the codes that made the patient eligible; for an exclusion criterion, the codes that disqualified them. Empty when nothing matched decisively.
Plural, and complete: the filters used to stop at their first decisive match, so the evidence could name only one of several qualifying codes. They now scan every entry. The verdict is unchanged either way — the first match already settled it — so this widens what is reported without moving any patient between states.
pending_laterality_review: Codes that matched a study-eye list but whose own laterality could not be inferred, so they could not decide the verdict. Their presence is why the criterion's state follows the configuredeligible_on_*_codes_lat_unknownrather than the codes themselves — a reviewer resolves them by hand. Reported only when nothing qualified: a populatedmatched_codessettles the criterion, which makes an indeterminate code moot.matched_values: The measurements that qualified, for a criterion whose verdict turned on an observation's own value. Empty or absent for every other code-list criterion, which has no measurement to report — a condition code either is or is not present.
Reported alongside matched_codes rather than instead of it: the code says which concept qualified, the value says what it read.
pending_date_review: Codes that matched an exclusion list carrying a recency window, but whose event date is unknown, so recency could not be confirmed. Exclusion-only: the inclusion filter has no windows. Reported under the same condition aspending_laterality_review.
Variables
- static
matched_codes : list[CodeMatch]
- static
matched_values : list[MeasuredValue]
- static
pending_date_review : list[str]
- static
pending_laterality_review : list[str]
CodeSpec
class CodeSpec(**data: Any):One glob-capable code, optionally pinned to its own code system.
Lets a _CodeAddressedLeaf's code list mix codes from more than one
system — e.g. an ICD-10 code alongside a SNOMED one — which a single
leaf-level code_system shared across every code cannot express.
Arguments
code: The code to match, glob-capable (e.g."E11*").code_system: This code's own system (e.g."icd10","snomed"), orNoneto fall back to the leaf's owncode_systemdefault (see_CodeAddressedLeaf.codes). A short form is resolved to its canonical URI (see_resolve_code_system).
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
CodeValueBound
class CodeValueBound(**data: Any):A comparison against an observation entry's own resolved value.
Six of FHIR's eleven Observation.value[x] shapes resolve to a shape
this class can compare against:
valueQuantity. A number with aunit.valueBoolean.valueInteger.valueString.valueTimeresolves the same way. A time string compares the same way as any other string.valueCodeableConcept. Matched structurally, throughCodeableConceptBound(seevaluebelow).
The remaining shapes never resolve: valueRange, valueRatio,
valuePeriod, valueDateTime, and valueSampledData. An entry
carrying one of these never qualifies against a CodeValueBound. These
shapes are not yet supported.
Arguments
op: The comparison operator. ReusesColumnCriterion.op's exact symbol set, from the sameOPERATOR_SPECStable (bitfount.steps.types.operators). Also reuses that table's per-dtype legality rule. For example, an ordering operator is illegal against astrorboolvalue. The rule is checked per entry at match time, not once at parse time. AColumnCriterionknows its column's dtype up front. An entry's own resolved value shape is only known once matching starts.
Which comparisons op allows depends on value's shape:
- Against a
valueString,"==","!=","in", and"not in"matchvalueas a glob rather than by plain equality. Only*acts as a wildcard, matched withfullmatch.CodeCriterion.codealready uses this convention. A target with no*behaves as plain equality. So a compound free-text value can match on one part of it. For example, the BCVA reading"20/60 (30.0) 0.67pt 0.18M -1"matchesvalue="*20/60*"without matching the whole string. - Against a
CodeableConceptBoundvalue, only"=="and"!="are legal. Ordering and membership do not apply to a structural match. value: The threshold or target value, or aCodeableConceptBoundthat matches avalueCodeableConceptentry structurally.unit: A bound on the entry's ownunit, viaUnitBound(see its own docstring for why this needs to be an object rather than a barestr | None). Absent: unit checking is skipped entirely, so avalueQuantityentry qualifies on its number alone, whatever unit it does or does not state.UnitBound(value=None)(i.e.unit: {}): the entry must itself state no unit at all — the safer, opt-in alternative to leavingunitunset.UnitBound(value="mg"): the entry's ownunitmust equal"mg"exactly. Must be unset whenvalueis a boolean or a string, since neither carries a unit of its own. An integervaluemay carry a unit: a whole-number quantity, like1 logMAR, is a legalvalueQuantity, and JSON gives no way to tell it from avalueInteger. Must also be unset whenvalueis aCodeableConceptBound.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
model_config
- static
op : Literal['>', '>=', '<', '<=', '==', '!=', 'in', 'not in']
- static
unit : UnitBound | None
- static
value : bool | int | float | str | list[str | int | float] | CodeableConceptBound
CodeableConceptBound
class CodeableConceptBound(**data: Any):A bound on an observation entry's valueCodeableConcept.
Mirrors the real FHIR CodeableConcept shape (see
hl7.org/fhir/R4/datatypes.html#CodeableConcept):
a concept expressed as a coding (system/code/display) and/or
free-text text. Each field you set narrows the match. Each field
left unset adds no constraint.
Arguments
coding: An optional bound on the entry'ssystem/code/display(seeCodingBound). Unset imposes no constraint on coding.text: Match against the entry's own free-texttext, as a*-glob.
CodingBound
class CodingBound(**data: Any):A bound on one CodeableConcept.coding entry.
Every field left unset imposes no constraint; each field that is set narrows the match further — the fields are AND'd together, not OR'd.
Arguments
system: Exact match against the coding'ssystemURI. Not glob-capable — asystemis an identifier, not free text.code: Match against the coding'scode, as a*-glob (the same conventionCodeCriterion.codeuses), e.g.code="8302-*". A target with no*behaves as plain equality.display: Match against the coding'sdisplay, as a*-glob.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
code : str | None
- static
display : str | None
- static
model_config
- static
system : str | None
ColumnCriterion
class ColumnCriterion(**data: Any):A generic, column-addressed eligibility criterion.
The expandable escape hatch: match any produced column by name without a
dedicated typed field. Prefer a typed CriteriaMatchConfig field when one
exists for the column.
Arguments
column: The DataFrame column to match against.op: Comparison operator applied ascolumn op value. Symbols only — a deliberately narrower surface than the alias set aColumnFilteraccepts.value: The threshold/target value, or aDerivedFromColumnspec resolving it from another column instead of a literal.grain:scanorpatient; whenNone, resolved from the field catalog.node: Discriminator identifying this as acolumn-typecriteria_treenode; irrelevant outside a tree (everycolumn_criteriaentry also carries it, unused).
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
column : str
- static
grain : CriterionGrain | None
- static
model_config
- static
node : Literal['column']
- static
op : Literal['>', '>=', '<', '<=', '==', '!=', 'in', 'not in']
- static
value : bool | int | float | str | list[str | int | float] | DerivedFromColumn
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
CountSelector
class CountSelector(**data: Any):Reduce candidate entries by requiring at least min_count of them.
Arguments
min_count: The minimum number of qualifying entries required.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
CriteriaEvaluation
class CriteriaEvaluation(**data: Any):One evaluated subject: its identity paired with its per-criterion outcomes.
Arguments
identity: The evaluated subject's identity.outcomes: The per-criterion outcomes (one per filter/bound; a range field contributes two, sharing anoutput_field). Never contains acriteria_treeleaf outcome onceattach_criteria_tree_verdicthas run — those live intree_node_outcomesinstead, alongside every combinator's own outcome, plus one entry here (the root's, duplicated) for the tree's own combined state.tree_node_outcomes: Everycriteria_treenode's own outcome — leaf and combinator alike, including the root — in post-order (children before parents). Leaves are moved here out ofoutcomesbyattach_criteria_tree_verdictsomatches_allnever re-ANDs an individual branch's leaf (which would defeat OR/AT_LEAST/NOT); every combinator's outcome is synthesised fresh, withgrain/provenancederived from its children (see_combinator_outcome) rather than fixed. Empty when nocriteria_treeis configured.review_flags: Every triggeredreview_reasonfrom the tree (leaf or group) whose own state resolved topass, in tree order, without deduplication. Populated byattach_criteria_tree_verdict; a downstream triage signal only — never affectsmatches_all.tree_score: The root node's own resolved[0, 1]score (seeCriterionOutcome.score), orNonewhen nocriteria_treeis configured. Entirely additive tomatches_all— a ranking signal, never a substitute for the PASS/FAIL gate.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
identity : SubjectIdentity
- static
model_config
- static
outcomes : list[CriterionOutcome]
- static
review_flags : list[str]
- static
tree_node_outcomes : list[CriterionOutcome]
- static
tree_score : float | None
-
matches_all : bool- Whether every criterion passed (reproducesFILTER_MATCHING_COLUMN).Any non-
pass(fail or unknown) means the row does not match; an empty outcome list (no active filters) is vacuouslyTrue.Returns:
Trueiff every outcome's state ispass.
CriterionFamily
class CriterionFamily(*args, **kwds):Which kind of criterion this is — the semantic family it belongs to.
Declared by the build site in build_eligibility_filters, not derived, and
that is the point: the families are a semantic partition, so two of them
are mechanically indistinguishable. A CODE_LIST and an APPOINTMENT
criterion are both patient-grain, EHR-provenance MethodFilters with no
recorded operator; nothing about the filter tells them apart. Only the
producer knows which it built.
Orthogonal to the three axes already carried:
CriterionKind— which filter type ran (column/method).CriterionGrain— cardinality (per-scan vs per-patient).CriterionProvenance— data source (scan vs EHR).
A family implies constraints on all three (a CODE_LIST is always a
patient-grain EHR method filter) but none of them recovers the family.
Its purpose on the wire is to tell a consumer how to read value and
which fields to expect populated, without inferring it from output_field
spellings. The patient API's EligibilityCriterion variants are keyed to
these members one-to-one.
Subclasses str so members serialise straight into JSON evidence.
Ancestors
Variables
- static
APPOINTMENT
- static
CODE_LIST
- static
COMPOUND_SCAN
- static
GENERIC_COLUMN
- static
GENERIC_METHOD
- static
PATIENT_METRIC
- static
SCAN_METRIC
CriterionGrain
class CriterionGrain(*args, **kwds):Whether a criterion is evaluated per scan or per patient.
Ancestors
CriterionKind
class CriterionKind(*args, **kwds):Which filter type produced a CriterionOutcome.
The explicit filter-type discriminator consumers branch on: COLUMN for a
ColumnFilter outcome, METHOD for a MethodFilter outcome. Subclasses
str so members serialise straight into JSON evidence.
Ancestors
CriterionOutcome
class CriterionOutcome(**data: Any):One criterion's outcome for one row, carrying enough to regenerate any string.
Arguments
config_field: OriginatingCriteriaMatchConfigfield, orNoneif unmapped.output_field: Logical field name / evidence key (filter column or name).column_name: Exact flat-df column name the filter would write (for tabulation).operator: TheColumnFilteroperator (">", ...);Nonefor aMethodFilterthat does not know its own operator.operand: The configured value the operator compared against — the comparison's right-hand side. Namedoperandrather thanthresholdbecause it is not always a bound:in/not input the whole candidate list here.Nonefor a filter with no recorded comparison.value: The observed value on the row, orNoneif missing/not applicable. AColumnFilterreports its cell; a code-listMethodFilterreports itsCodeMatchEvidence— the codes that drove the verdict — so a structured verdict lands in the same field as a scalar one rather than in a field of its own. Readfamilyto know which to expect. Every otherMethodFilterhas no single observed value and reportsNone.state:pass/fail/unknown(producer never emitsna).context: Runtime context that cannot be regenerated, as prose (e.g. matched codes). Consumed by the report's context column; for a code-list criterionvaluecarries the same information structurally.failed_message: AMethodFilter's regenerated failure reason (its static message, or a "missing values"/"unable to determine" variant), orNonefor aColumnFilteroutcome.grain:scanorpatient— the cardinality (per-scan vs per-patient), not the data source (seeprovenance).provenance:scanorehr— the data source (distinct fromgrain). Defaults tounknown; stamped bybuild_criterion_outcomesfrom the source column, not by the filter (age can be either source).unit: The target column's display unit (e.g."mm²"), orNonewhen the field is unitless or uncatalogued. Likeprovenance, not a filter attribute — stamped bybuild_criterion_outcomesfrom the target column'sFieldSpecin the field catalog.absent_column: For aColumnFilteroutcome whose target column was entirely absent from the row (as opposed to present with aNaNvalue), the raw configured column name to render into the regenerated "not found" reason instead of its display rename.Nonewhen the column was present (even ifNaN), and alwaysNonefor aMethodFilteroutcome.matched_column: The source column this row's verdict actually read, for a filter that can read more than one. Set only by the observation filter, which spansOBSERVATIONS_COLUMNandSUPPLIED_OBSERVATIONS_COLUMN, and only when the row produced qualifying candidates.build_criterion_outcomesprefers it over the filter-levelprovenance_columnwhen stampingprovenance, which is what makes provenance per-row rather than per-filter for a multi-column filter.Nonefor every single-column filter and for a row that matched nothing — the filter-level constant is used then.kind:columnfor aColumnFilteroutcome,methodfor aMethodFilteroutcome. The explicit filter-type discriminator consumers branch on.family: The semantic criterion family (seeCriterionFamily), carried through from the filter that produced this outcome.Noneonly for a filter built without one — a directly constructed filter in a test, or a user-authoredColumnFilterin a task config, neither of which comes frombuild_eligibility_filters.tree_node_id: Thecriteria_treeleaf id this outcome was built from, orNonefor an outcome not built from a tree leaf.attach_criteria_tree_verdictuses this to move the outcome out ofCriteriaEvaluation.outcomesintotree_node_outcomes, so a tree leaf's own PASS/FAIL is never re-ANDed bymatches_all. A combinator's own synthesised outcome (see_combinator_outcome) also sets this, to its own node id, so every entry intree_node_outcomesis addressable the same way.tree_group: The tree node's resolved (inherited)groupreporting label, stamped byevaluate_criterion_treefor every node — leaf or combinator.Nonewhen neither the node nor any ancestor set one, and alwaysNonefor a non-tree outcome.
Not an identity: group does not override, so every node beneath a region root shares that root's exact string (see _check_group_inheritance). Uniqueness is enforced among region roots, not among the leaves under one.
tree_evidence_id: The tree node's ownevidence_id, stamped byevaluate_criterion_tree.Nonefor a node that sets none and for every non-tree outcome. Unliketree_groupthis is not inherited and is unique tree-wide, so it identifies the one leaf that set it.score: This node's own resolved[0, 1]score (seeevaluate_criterion_tree), orNoneif this node has not resolved enough to score yet, or if it is not part of acriteria_treeat all.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
absent_column : str | None
- static
column_name : str
- static
config_field : str | None
- static
context : str | None
- static
failed_message : str | None
- static
family : CriterionFamily | None
- static
grain : CriterionGrain
- static
kind : CriterionKind
- static
matched_column : str | None
- static
model_config
- static
operand : Any
- static
operator : str | None
- static
output_field : str
- static
provenance : CriterionProvenance
- static
score : float | None
- static
state : CriterionState
- static
tree_evidence_id : str | None
- static
tree_group : str | None
- static
tree_node_id : str | None
- static
unit : str | None
- static
value : Any
CriterionProvenance
class CriterionProvenance(*args, **kwds):Where a criterion's evaluated data was pulled from — its data source.
The source axis, orthogonal to CriterionGrain (the cardinality axis,
per-scan vs per-patient). A patient-grain criterion can still be
scan-provenance: age computed from a DICOM birth-date is grain=patient, provenance=scan. Subclasses str so members serialise straight into JSON
evidence. Not a filter attribute — resolved in build_criterion_outcomes
from the source column each value was built from.
Ancestors
CriterionState
class CriterionState(*args, **kwds):A single criterion's evaluated state.
na = not applicable to this row (a patient-level criterion on a scan row);
only consumers assign na, never the producer. Subclasses str so members
serialise straight into JSON evidence.
Ancestors
DateWindow
class DateWindow(**data: Any):A window a matching event's date must fall within.
Each bound is independent and optional: a literal date pins it
exactly; a RelativeDate pins it relative to the task's own
reference date at match time (see _code_date_bounds); None leaves
that side open (start unset means all history; end unset means no
upper bound). Named after FHIR's own Period datatype (start/end),
which this plays the same role for.
Each of start/end can independently be absolute or relative, so
e.g. "between 2 years ago and 6 months ago", or "more than 4 weeks
ago" (end a RelativeDate, start left as all history), are both
expressible.
Arguments
start: The window's start.end: The window's end.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
end : datetime.date | RelativeDate | None
- static
model_config
- static
start : datetime.date | RelativeDate | None
DerivedFromColumn
class DerivedFromColumn(**data: Any):A ColumnCriterion.value resolved from another column, not a literal.
resolved = ceil(numerator / row[divisor_column]), clamped to minimum
when minimum is set. Exists for a criterion whose bound is not a fixed
value: the config author does not supply it directly — criteria_matching
computes it per row, from a second column. n_scan_*_typical_width_micrometers
is the existing example: it needs ceil(width / slice_thickness), and a
plain literal value cannot express that. slice_thickness varies by
device and protocol, so it cannot be a fixed config value either.
Arguments
numerator: The constant divided by the value indivisor_columnto resolve the comparison bound.divisor_column: The column read (from the same row) as the divisor.round: How the division result is rounded before comparison. Only"ceil"is supported: the one existing use rounds up, since a partial unit never satisfies a "spans at least N of them" bound.minimum: A floor applied to the resolved value after rounding, orNonefor no floor. A numerator of0would otherwise resolve to0(ceil(0 / x) == 0), making the comparison vacuous.on_unusable_divisor: The state to resolve to whendivisor_columnis missing,NaN, or0—True(pass),False(fail), orNone(unknown). No resolved value can be computed in that case. There is no correct default for every criterion: whether "unevaluable" means "don't know" or "don't exclude" depends on the specific criterion, not on this mechanism.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
divisor_column : str
- static
minimum : float | None
- static
model_config
- static
numerator : float
- static
on_unusable_divisor : bool | None
- static
round : Literal['ceil']
DeviceCriterion
class DeviceCriterion(**data: Any):An EHR device record, matched by its own type code or its text.
FHIR R4 Device (https://hl7.org/fhir/R4/device.html) carries no field
meaning "when this became relevant to the patient" (only
manufactureDate/expirationDate, neither clinical) — unlike every
other code-addressed leaf, this one has no date_range at all, not
merely an unset one. selector is declared fresh here rather than
inherited and narrowed: a mutable pydantic field's type must stay
invariant across a class hierarchy, so narrowing an inherited field in a
subclass is unsound and pyrefly rejects it
(bad-override-mutable-attribute). Declaring it fresh here instead
excludes "latest"/"earliest" (neither can select a "most recent"
entry with no date to order by) and "all" (a poor fit for trial
criteria — a patient's devices are typically unrelated to each other,
e.g. a pacemaker and a hip implant — and mathematically identical to
"any" here regardless, since with no secondary per-entry check both
reduce to "at least one candidate").
text matches the strings that name the device — its code_text and
each codings[].display — rather than its code, and applies whether or
not the device is coded. Given alone it is an alternative to code;
given alongside one it narrows that match rather than widening it.
It exists because for some backends a device has no code to match at all. NextGen builds a device record from the FDA GUDID catalogue, which carries no SNOMED CT concept, so its devices are named by a Global Medical Device Nomenclature term and coded not at all — a code-only criterion can never match one. GMDN is a controlled vocabulary of device types, so matching its terms is closer to matching a code than free text usually is.
code is optional here, as on AllergyCriterion and for the same
reason: a text-only criterion has to be expressible, and for a
GUDID-sourced device it is the only expressible one. Supplying neither
is rejected — a leaf narrowed only by selector would count devices of
any kind, which is not a question trial criteria ask. When both are
given they are additional restrictions, not alternatives: an entry must
match both.
Arguments
node: Discriminator identifying this as adevice-typecriteria_treenode.code: As_BareCodeAddressedLeaf.code; omit it for a text-only criterion.text: Free text matched against the strings naming the device, glob-capable the same waycodeis (e.g."*intraocular lens*"), or a list of alternatives. Matched case-insensitively, unlikecode, since the casing of a device name depends on which backend answered. Matched against the whole string, so a bare"intraocular lens"does not match"Anterior-chamber intraocular lens, pseudophakic"— wrap it in*to match within. Searched acrosscode_textand everycodings[].display; see_code_text_candidates.selector:"any", or aCountSelectorrequiring at leastNqualifying devices (see above for why"all"/"latest"/"earliest"are excluded).
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
criterion_noun : ClassVar[str]
- static
model_config
- static
node : Literal['device']
- static
selector : Union[Literal['any'], CountSelector]
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
FieldSpec
class FieldSpec(**data: Any):A single matchable field a source can contribute to criteria matching.
Arguments
name: The flat DataFrame column name a criterion targets.dtype: A coarse type tag ("float","int","bool","str").unit: Human-readable display unit (e.g."mm²"), orNonewhen unitless. Carries the character a consumer should render; an ASCII fallback belongs to whichever sink cannot take it, not here.grain: Whether the field is per-scanor per-patient.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
dtype : str
- static
grain : CriterionGrain
- static
model_config
- static
name : str
- static
unit : str | None
MatchStrategy
class MatchStrategy(*args, **kwds):How a generic column_criteria is matched against its column.
The dispatch discriminator _build_generic_filter branches on when turning
a criterion into a concrete filter — it names which matching mechanism
applies, not merely which factory is called. Orthogonal to CriterionKind
(the filter-type an outcome carries, column vs method): both
SCALAR_MATCH and COLLECTION build a MethodFilter, so this three-way
choice does not collapse onto that two-way one. Subclasses str so it
compares equal to the historical literal keys.
Ancestors
MeasuredValue
class MeasuredValue(*args, **kwargs):One observation entry's own measurement, as evidence.
An observation criterion's verdict turns on a number, not merely on a
code being present, so reporting only the code leaves a reviewer unable
to see what was measured. A BCVA criterion reporting pass says
nothing about whether the study eye read 20/40 or 20/200; this carries
the reading itself.
A TypedDict, and total=False, for the same reasons as
CodeMatchEvidence, which nests it: the value is persisted as JSON, and
a row written before this existed carries no key — read every key with
.get.
Attributes
code: The code the entry was recorded under.value: The entry's own resolved value — a number for a quantity, a string for a free-text reading (a Snellen"20/40"). AvalueCodeableConceptcarries no value field of its own, so it reports itstext, elsedisplay, elsecode.unit: The entry's unit, when it stated one.comparator:"<","<=",">"or">="when the entry stated a limit rather than a measurement,Nonewhenvalueis the measurement itself. A below-detection result reports the detection limit as itsvalue, so without this a reader sees an undetectable troponin as a reading of exactly0.02.date: The entry's date as"%Y-%m-%d", when it stated one. Which reading qualified matters as much as the value: a window satisfied by a five-year-old measurement is a different fact about a patient than one satisfied last week.laterality:"L"or"R"when the entry resolves to exactly one eye,Nonewhen it is bilateral or indeterminate.scopesays whether the eye had to match the scan's; this says which eye was actually read, which a reviewer needs when both eyes were measured at one visit and only one of them qualified.scope:any_eyefor an entry qualifying regardless of laterality,study_eyeorfellow_eyefor one whose laterality had to include the scan eye or the other eye (and did) — the same distinctionCodeMatch.scopedraws.
Variables
- static
code : str
- static
comparator : str | None
- static
date : str | None
- static
laterality : str | None
- static
scope : Literal['any_eye', 'study_eye', 'fellow_eye']
- static
unit : str | None
- static
value : float | str | None
MedicationCriterion
class MedicationCriterion(**data: Any):An EHR medication, matched by code or text, optionally bounded by dosage.
Arguments
node: Discriminator identifying this as amedication-typecriteria_treenode.code: As_BareCodeAddressedLeaf.code; omit it for a text-only criterion.text: As_TextAddressedLeaf.text, searched across the entry'scode_textand everycodings[].display. A medication is not always coded, and a drug recorded as free text carries only its name.dose: An optional bound (or list of bounds) on the medication's own dosage (seeMedicationDoseBound). When set, a matching code entry additionally qualifies only if any of its dosage instructions satisfies every given bound. A list is not the same as oneMedicationCriterionleaf per bound under anAndGroup: each leaf there would find its own qualifying entry independently, so e.g. a dose_quantity leaf and a frequency leaf could each be satisfied by a different instruction on the same entry. A list here pins every bound to one instruction at once — e.g. "40mg, given at least twice daily," not "40mg on some instruction, and at least twice daily on some (possibly different) instruction." When unset, behaves exactly likeCodeCriterion: presence/date/laterality only, dosage ignored.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
criterion_noun : ClassVar[str]
- static
dose : Union[MedicationDoseQuantityBound, MedicationFrequencyBound, list[MedicationDoseQuantityBound | MedicationFrequencyBound], ForwardRef(None)]
- static
model_config
- static
node : Literal['medication']
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
MedicationDoseQuantityBound
class MedicationDoseQuantityBound(**data: Any):A comparison against one dose_and_rate reading's own dose_quantity.
Arguments
field: Discriminator identifying this as adose_quantitybound.dose_type: Whichdose_and_ratereading to use —"ordered"(the prescriber's own instruction, sometimes per-weight) or"calculated"(a separately computed absolute amount, which also covers aMedicationAdministration-sourced reading: an exact amount actually given, not a prescriber's own instruction). These are different measurements, not interchangeable variants of one fact. A config author who wants "either reading qualifies" writes two leaves under anOrGrouprather than picking one.op: The comparison operator."in"/"not in"are deliberately excluded, unlikeCodeValueBound.op's full set: a dose is a continuous quantity, not a small enumerated set of values to check membership against, and "one of these exact strengths" (the one real case for it) is a config author writing oneOrGroupleaf per strength rather than a membership list.value: The threshold value.unit: The unitdose_unitmust equal. Required rather than optional, unlikeCodeValueBound.unit: a dose reading in the wrong unit is not a "probably fine" comparison the way a lab value might be."ordered"and"calculated"readings routinely differ by exactly a unit (mg/kg vs mg).
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
dose_type : Literal['ordered', 'calculated']
- static
field : Literal['dose_quantity']
- static
model_config
- static
op : Literal['>', '>=', '<', '<=', '==', '!=']
- static
unit : UnitBound
- static
value : bool | int | float | str
MedicationFrequencyBound
class MedicationFrequencyBound(**data: Any):A comparison against an instruction's own frequency, pinned to one period.
Arguments
field: Discriminator identifying this as afrequencybound.op: The comparison operator, asMedicationDoseQuantityBound.op—"in"/"not in"excluded for the same reason: "twice or three times daily" is anOrGroupof two==leaves, not a membership list.value: The threshold value.frequency_period: Theperiodan entry's ownfrequencymust be measured over.frequencyalone is not a clinically meaningfulbound: "at least twice" could mean twice daily or twice weekly, and those are different prescriptions. This pins the comparison to one instruction's ownperiod, exactly (not compared viaop), the same waydose_typepins onedose_and_ratereading.frequency_period_unit: Theperiod_unitan entry's ownfrequencymust be measured over, matched exactly alongsidefrequency_period.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
field : Literal['frequency']
- static
frequency_period : float
- static
frequency_period_unit : str
- static
model_config
- static
op : Literal['>', '>=', '<', '<=', '==', '!=']
- static
value : bool | int | float | str
MethodOutcome
class MethodOutcome( passed: ForwardRef('bool | None'), context: ForwardRef('str | None') = None, code_match: ForwardRef(''codeBlockAnchor[CodeMatchEvidence](/api/bitfount/steps/criteria_matching_types#codematchevidence)' | None') = None, matched_column: ForwardRef('str | None') = None, measured_value: ForwardRef('float | int | str | None') = None, definitive: ForwardRef('bool') = False,):What a MethodFilter's per-row callable returns.
A NamedTuple with defaults, so the plain (eligible, context) tuple every
existing filter returns is still a valid return value and needs no change:
MethodFilter.evaluate_row normalises whatever it gets through
MethodOutcome(*result). A filter with more to report sets the later
fields.
Attributes
passed: The verdict —Nonefor unknown (e.g. the filter's data is absent for this row), which never becomespass.context: Human-readable runtime detail that cannot be regenerated from the filter's configuration (the report's context column renders this).code_match: The same detail structurally, for a code-list filter. Unlikecontext, this is persisted into the criterion's evidence and served by the patient API.matched_column: For a filter that reads more than one source column, the column this row's qualifying candidates came from. Only the observation filter sets it (it spansOBSERVATIONS_COLUMNandSUPPLIED_OBSERVATIONS_COLUMN); it is what letsbuild_criterion_outcomesstamp provenance per row instead of per filter.Nonewhen the filter reads one column, or when this row produced no candidates to attribute.measured_value: The scalar this row measured, for a filter whose verdict turns on a number the evidence should report (a distance, a width). ReachesCriterionOutcome.valuewhen there is nocode_match; a filter produces one or the other, never both.definitive: Set by a filter that reached this verdict deliberately while a required column is null — a measured absence settling the question rather than a failure to measure. It suppresses the "missing values" rewrite offailed_message, which otherwise reports such a row as unmeasured.
Variables
code_match : CodeMatchEvidence | None- Alias for field number 2
context : str | None- Alias for field number 1
definitive : bool- Alias for field number 5
matched_column : str | None- Alias for field number 3
measured_value : float | int | str | None- Alias for field number 4
passed : bool | None- Alias for field number 0
NotGroup
class NotGroup(**data: Any):A criteria_tree node inverting its single child's state.
Arguments
child: The node to invert.passandfailswap;unknown/naunchanged.review_reason: SeeColumnCriterion.review_reason.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
child : ColumnCriterion | CodeCriterion | ObservationCriterion | MedicationCriterion | AllergyCriterion | DeviceCriterion | AppointmentWindowCriterion | AndGroup | OrGroup | AtLeastGroup | NotGroup
- static
model_config
- static
node : Literal['not']
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
ObservationCriterion
class ObservationCriterion(**data: Any):An EHR observation code, optionally bounded by its own resolved value.
Arguments
node: Discriminator identifying this as anobservation-typecriteria_treenode.code: As_BareCodeAddressedLeaf.code; omit it for a text-only criterion.text: Free text matched against the strings naming the observation, glob-capable the same waycodeis (e.g."*haemoglobin a1c*"), or a list of alternatives. Matched case-insensitively, unlikecode, since the casing of a test name depends on which backend answered. Matched against the whole string, so a bare"troponin"does not match"Troponin I, high sensitivity"— wrap it in*to match within. Searched acrosscode_textand everycodings[].display; see_code_text_candidates.
It exists because a lab result need not be coded at all: a NextGen result carries a loincCode only when the interface populated one, and a manually entered result generally has none, leaving the test name as the only thing to match on.
value: An optional bound on the observation's own value (seeCodeValueBound), or a list of bounds every one of which the same entry must satisfy. When set, a matching code entry additionally qualifies only if its value satisfies the bound(s). When unset, behaves exactly likeCodeCriterion: presence/date/laterality only, value ignored.
A list is how a two-sided window is expressed, and expressing one as two sibling leaves under an AndGroup does not work. Every selector except CountSelector reduces to "at least one candidate entry qualifies" (see _build_code_filter), so two leaves ask whether some entry cleared the lower bound and whether some entry cleared the upper — satisfiable by two different readings on different dates. A patient whose study eye read 0.18 logMAR once and 1.40 logMAR another time passes both halves of a >= 0.30/<= 1.00 window without ever having had a reading inside it. Listing both bounds on one leaf checks them against a single entry, which is what "a measurement in this range" means.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
criterion_noun : ClassVar[str]
- static
model_config
- static
node : Literal['observation']
- static
value : CodeValueBound | list[CodeValueBound] | None
-
value_bounds : list[CodeValueBound]-value, normalized to the list of bounds ANDed against one entry.Empty when no value bound is configured, so a caller can iterate unconditionally rather than branching on
None.
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
OrGroup
class OrGroup(**data: Any):A criteria_tree node requiring at least one child to pass.
Arguments
children: The child nodes; at least one required.review_reason: SeeColumnCriterion.review_reason. Collected whenever this node passes, even if an unflagged sibling child also passes: flagging more than the minimum set of passing nodes is intentional, since missing a genuinely lower-priority match costs more than an unnecessary review.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:This function is meant to behave like a BaseModel method to initialise private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments
self: The BaseModel instance.context: The context.
RelativeDate
class RelativeDate(**data: Any):A date expressed relative to another instant — this many units before/after it.
Arguments
unit: The unittimeis measured in.time: How many ofunit. Must be positive.direction: Which way this points relative to the instant it's resolved against —"past"(before it) or"future"(after it). Defaults to"past".
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
direction : Literal['past', 'future']
- static
model_config
- static
time : int
- static
unit : Literal['d', 'w', 'm', 'y']
SubjectIdentity
class SubjectIdentity(**data: Any):Identity of the subject evaluated on one row.
Every field is already known to criteria_matching while it evaluates the
row. scan_id/study_date/laterality are None on the non-file-system
(EHR) grain, where a row is a patient rather than a scan.
Arguments
bitfount_patient_id: The Bitfount patient ID, orNonewhen the row has none. Absence isNonerather than the empty string it used to be: an ID is derived from a name and a date of birth, and a name that cannot be reduced to a given and a family name yields no ID (seebitfount.federated.algorithms.ophthalmology.dataframe_generation_extensions.name_id_key). An empty string is a value, and values match each other — pandas joins two rows sharing""as if they were one patient, and a consumer that forgets to check gets no complaint from the type checker.Nonemakes the absence a distinct state the checker enforces at every point of use.scan_id: The scan filename, orNoneon the EHR grain.study_date: The scan study date (YYYYMMDD), orNone.laterality: The scan laterality, orNone.name: Display name from the row (a fallback; EHR prefers its own lookup).mrns: Medical record number(s) found on the row.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
bitfount_patient_id : str | None
- static
laterality : str | None
- static
model_config
- static
mrns : list[str]
- static
name : str | None
- static
scan_id : str | None
- static
study_date : str | None
UnitBound
class UnitBound(**data: Any):The unit a valueQuantity entry's own unit must equal, or lack.
A bare str | None cannot tell "don't care about unit" apart from
"the entry must itself carry no unit": both would read as None. This
object gives the two states distinct shapes instead: absent on
CodeValueBound.unit means "don't care"; present, with value unset,
means "the entry's own unit must be unset too."
Arguments
value: The exact unit the entry's ownunitmust equal, orNoneto require that the entry itself states no unit at all.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.