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,) ‑> CriterionOutcome:Recursively reduce a criteria_tree node, building one outcome per node.
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.
Returns
The node's own outcome (its combined state, 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]: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 | 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 | 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 its own code or a reaction substance.
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.
Arguments
node: Discriminator identifying this as anallergy-typecriteria_treenode.
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.
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.
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.
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.
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 code, matched by presence/date/count only.
Arguments
resource: Which resourcecodeis matched against.node: Discriminator identifying this as acode-typecriteria_treenode.
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.scope:any_eyefor a code qualifying regardless of laterality,study_eyefor one whose inferred laterality had to include the scan eye (and did).
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).
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, integer, or string, since none of those carry a unit of their own. 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.
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]
-
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.
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
state : CriterionState
- 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.
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").
Arguments
node: Discriminator identifying this as adevice-typecriteria_treenode.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
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.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_eyefor one whose laterality had to include the scan eye (and did) — the same distinctionCodeMatch.scopedraws.
Variables
- static
code : str
- static
date : str | None
- static
laterality : str | None
- static
scope : Literal['any_eye', 'study_eye']
- static
unit : str | None
- static
value : float | str | None
MedicationCriterion
class MedicationCriterion(**data: Any):An EHR medication code, optionally bounded by its own dosage.
Arguments
node: Discriminator identifying this as amedication-typecriteria_treenode.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
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,):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 that has structured detail to report adds
the third field.
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.
Variables
code_match : CodeMatchEvidence | None- Alias for field number 2
context : str | None- Alias for field number 1
matched_column : str | None- Alias for field number 3
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 | 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.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
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', '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.