eligibility_functions
Pure logic shared by the scan/ehr/patient_eligibility steps.
Derives per-criterion evidence and per-scan / per-patient eligibility status.
build_scan_evidence, build_patient_level_evidence and roll_up_patient all
consume criteria_matching's structured CriteriaEvaluation output (directly,
or via the stored ScanVerdict evidence it produces). No I/O.
Vocabulary:
- Criterion state (
CriterionState):pass/fail/unknown/na(na= not applicable to a scan, i.e. a patient-level criterion). - Row status (scan / patient):
EligibilityStatus—eligible/ineligible/unknown.
Module
Functions
all_criteria_met
def all_criteria_met( statuses: Iterable[CriterionState],) ‑> bitfount.cache.types._eligibility_shared.EligibilityStatus:Strict combine over criterion states -> a row status.
na states are ignored. Any fail -> ineligible; else any unknown ->
unknown; else eligible (including the empty/all-na case).
Arguments
statuses: Criterion states (pass/fail/unknown/na).
Returns
The rolled-up EligibilityStatus.
all_criteria_met_criterion
def all_criteria_met_criterion( states: Iterable[CriterionState],) ‑> CriterionState:Combine two criterion states for one merged field -> a criterion state.
Unlike all_criteria_met (which returns a row status), this stays in the
criterion vocabulary: any fail -> fail; else any unknown -> unknown;
else pass.
Arguments
states: Criterion states to combine.
Returns
A criterion state (pass/fail/unknown).
any_scan_qualifies
def any_scan_qualifies( scan_statuses: Iterable[EligibilityStatus],) ‑> bitfount.cache.types._eligibility_shared.EligibilityStatus:Lenient combine over scan statuses -> a patient status.
Any eligible -> eligible; else any unknown -> unknown; else
ineligible. Empty -> unknown.
Arguments
scan_statuses: Per-scan statuses (eligible/ineligible/unknown).
Returns
The rolled-up EligibilityStatus.
build_patient_level_evidence
def build_patient_level_evidence( evaluation: CriteriaEvaluation,) ‑> tuple[dict[str, bitfount.cache.types._eligibility_shared.CriterionEvidence], bitfount.cache.types._eligibility_shared.EligibilityStatus]:Build patient-level evidence + its local status from one evaluation.
Named for what it now covers. This was build_ehr_evidence while ehr was
the only non-imaging provenance; supplied is a second one, and folding it
in here rather than inventing a third evidence table is why the name had to
change. It feeds PatientLevelEligibilityRecord, which was always named for
the grain rather than the source, so the table needed no rename.
Emits an entry for each patient-grain criterion whose data did not come from
imaging: EHR age, code lists, and unlateralised supplied observations.
Scan-provenance patient-grain age is surfaced by the scan step, not here, and
so is a lateralised supplied observation — that is scan-grain, because the
study eye is resolved per scan (see build_scan_evidence).
The returned status is all_criteria_met over this step's own entries
only. It is local to this step, and it now spans two sources rather than one:
an EHR code list and a supplied BCVA both gate it. The patient_eligibility
reducer, not this status, computes the all-patient-grain verdict from the
merged tagged evidence (so scan-sourced age still gates).
Arguments
evaluation: A representative evaluation for the patient (patient-level criteria are constant across a patient's scans).
Returns
(criteria, status) over the patient-grain, non-imaging criteria only.
build_scan_evidence
def build_scan_evidence( evaluation: CriteriaEvaluation,) ‑> tuple[dict[str, bitfount.cache.types._eligibility_shared.CriterionEvidence], bitfount.cache.types._eligibility_shared.EligibilityStatus]:Build scan-provenance evidence + the scan-grain status from one evaluation.
Scoped mostly by provenance (scan), spanning both grains: scan-grain GA
metrics and scan-provenance patient-grain criteria (DICOM age). The returned
status is by grain: all_criteria_met over scan-grain entries only, so a
patient-grain criterion (age) never gates a per-scan verdict (the reducer
gates on it by grain).
supplied criteria are partitioned by grain instead, and that is the one
asymmetry in this partition. A supplied measurement is not imaging-derived,
so on provenance alone it belongs with the EHR criteria — but a lateralised
one (laterality.side != "either", hence grain == SCAN) is a per-eye
question and the study eye is resolved per scan, so it is kept here and gates
scan_status. An unlateralised supplied criterion is patient-grain and goes
to build_patient_level_evidence.
That asymmetry exists to close the gating hole this docstring used to warn
about. Partitioning purely by provenance lets a grain == SCAN, not-scan-provenance criterion fall through both steps: skipped here as
non-scan, and excluded from the reducer's patient-grain sum as scan-grain —
gating NOTHING, which reads downstream as passing. A lateralised supplied
observation criterion is exactly that shape, and it is the primary
enrichment use case (study-eye BCVA), so the hole stopped being hypothetical
and had to be closed rather than documented.
The grain == SCAN, provenance == EHR shape still does not occur — every EHR
filter is patient-grain — but the predicate below partitions it by grain too,
so introducing one no longer silently gates nothing.
Arguments
evaluation: The row'sCriteriaEvaluationfromcriteria_matching.
Returns
(evidence, scan_status).
combine_statuses_strict
def combine_statuses_strict( statuses: Iterable[EligibilityStatus],) ‑> bitfount.cache.types._eligibility_shared.EligibilityStatus:Strict combine over row statuses (eligible/ineligible/unknown).
Any ineligible -> ineligible; else any unknown -> unknown; else
eligible (empty -> eligible). Used to combine a patient's patient-level
verdict with their determining-scan verdict.
Arguments
statuses: Row statuses to combine.
Returns
The combined EligibilityStatus.
merge_criteria
def merge_criteria( ehr_criteria: Mapping[str, CriterionEvidence], scan_criteria: Mapping[str, CriterionEvidence],) ‑> dict[str, bitfount.cache.types._eligibility_shared.CriterionEvidence]:Merge ehr + scan-rollup criteria into the served map, guarding clashes.
Single run: the two keyspaces are disjoint (age has one provenance, surfaced
by one step). Federated multi-run: a key can appear in both with different
provenance (a scan pod's DICOM age and an aggregator's EHR age under one
task_hash); _resolve_clash picks one deterministically and warns.
Arguments
ehr_criteria: Thebuild_patient_level_evidencemap for the patient.scan_criteria: The determining scan's criteria (roll_up_patient).
Returns
The merged {output_field: CriterionEvidence} map.
roll_up_patient
def roll_up_patient( scans: Sequence[ScanVerdict],) ‑> PatientScanEligibilityRollup:Roll up a patient's scans into a determining-scan verdict + evidence.
Arguments
scans: OneScanVerdictper scan, whereevidenceis the output ofbuild_scan_evidence.
Returns
A PatientScanEligibilityRollup (carrying the determining scan's
scan-provenance criteria of both grains).
Classes
PatientScanEligibilityRollup
class PatientScanEligibilityRollup(**data: Any):A patient's eligibility rolled up from their per-scan verdicts.
Named for what it is: the scan-derived half of a patient's eligibility (the patient-level criteria are combined in on top of this by the step layer).
Attributes
status: The rolled-up patient status.determined_by_scan: The determining scan's ID, if any.evidence: The rollup evidence (scan-level criteria from the determining scan; patient-level criteria are merged in by the step layer).
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
determined_by_scan : str | None
- static
evidence : dict[str, typing.Any]
- static
model_config
- static
status : bitfount.cache.types._eligibility_shared.EligibilityStatus
ScanVerdict
class ScanVerdict( scan_id: ForwardRef('str'), study_date: ForwardRef('str | None'), status: ForwardRef('EligibilityStatus'), evidence: ForwardRef('Evidence'),):One scan's stored verdict, as roll_up_patient consumes it.
Attributes
scan_id: The scan's identifier.study_date: The scan's study date (YYYYMMDD), orNoneif unknown.status: The scan'sbuild_scan_evidencestatus.evidence: The scan's per-criterion evidence map.
Variables
evidence : dict[str, bitfount.cache.types._eligibility_shared.CriterionEvidence]- Alias for field number 3
scan_id : str- Alias for field number 0
status : bitfount.cache.types._eligibility_shared.EligibilityStatus- Alias for field number 2
study_date : str | None- Alias for field number 1