v1
Generic en-face lesion calculation step v1.
Module
Submodules
- bitfount.steps.lesion_calculation.v1.config - Config for the generic en-face lesion calculation step (v1).
- bitfount.steps.lesion_calculation.v1.functions - Cache-persistence helpers for the lesion_calculation step (v1).
- bitfount.steps.lesion_calculation.v1.preprocessing - Shape-based preprocessing for the lesion_calculation v1 step.
- bitfount.steps.lesion_calculation.v1.result - Result for the generic en-face lesion calculation step (v1).
- bitfount.steps.lesion_calculation.v1.task - Prefect task for the generic en-face lesion calculation step.
Functions
task_fn
def task_fn( datasource: BaseSource, config: LesionCalculationConfig, pathology_predictions: CacheAccessor, filenames: list[str], cache: CacheProtocol, task_hash: str, fovea_predictions: CacheAccessor | None = None, project_id: str | None = None, run_id: str | None = None,) ‑> LesionCalculationResult:Compute per-group lesion metrics for each file and persist them to cache.
Arguments
datasource: The datasource providing DICOM metadata (slice thickness, pixel spacing) for each file.config: Lesion calculation configuration, naming the groups to measure, the ROI radius, and the per-group lesion cap.pathology_predictions: Cache accessor for pathology model inference results (background stepga_inference.cache).filenames: List of file IDs to process.cache: Cache instance to persist the lesion metrics into.task_hash: Partition key for thelesion_calculationtable; a config change lands in a fresh partition.fovea_predictions: Optional cache accessor for fovea model inference results (background stepfovea_inference.cache).Noneis a supported state, not an error: a template without a fovea step still gets areas, volumes and lesion sizes, just no fovea-relative fields.project_id: Provenance only — the project that triggered this run. NOT part of the cache key: rows are keyed by(task_hash, file_id)so two projects on the same datasource share them.run_id: Optional provenance run ID.
Returns
LesionCalculationResult carrying the number of rows persisted and a
CacheAccessor scoped to this task_hash (lesion_calculation.cache).
Raises
KeyError: If the predictions frame lacks thefile_idcolumn the merge aligns on — a programming error, not a data condition.
Classes
Config
class Config(**data: Any):Config for the generic lesion calculation step.
Attributes
groups: Named label groups to measure. Each produces one column per entry inLESION_GROUP_METRIC_SUFFIXES, named<group>_<metric>. An empty map makes the step a no-op.roi_radius_mm: Radius in mm of a region of interest centred on the fovea landmark, orNoneto measure only the whole-scan aggregates. This is a RADIUS, not a diameter: a protocol naming a 3000 µm diameter region is configured as1.5.Nonewhen no fovea landmark is available for a scan, regardless of this setting.max_lesions_per_group: Cap on how many lesions ameasure_lesionsgroup reports in detail per scan; the largest are kept. Does not affect the group's own totals (total_area,num_lesions, …), which are never capped.angular_span_min_radius_mm: Cells closer than this to the fovea contribute no angle toaggregate_angular_span.None(the default) measures from the fovea outwards, which is the previous behaviour.
It exists because the span has no angular RESOLUTION close in: the B-scan sampling gives about degrees(slice_thickness / r) per sector end, so a single cell one column from the fovea subtends about 176 degrees and two of them pass a 270-degree threshold. That is the B-scan spacing, not a rounding error, and no in-plane exactness recovers it.
A value here changes who is recruited, so it is a clinical decision and belongs in a trial's config where it is visible, not in a geometry constant. It lives on the STEP rather than on a criterion because the measurement is genuinely restricted at compute time — the same reason roi_radius_mm does — which means changing it rotates the cache partition and recomputes every scan.
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.
Ancestors
Variables
- static
angular_span_min_radius_mm : float | None
- static
groups : dict[str, MaskGroupSpec]
- static
max_lesions_per_group : int
- static
model_config
- static
roi_radius_mm : float | None
Result
class Result(**data: Any):Result of a lesion calculation task.
records_stored reports how many per-file area rows were persisted to the
lesion_calculation cache table. The metrics themselves are not returned
in-memory: this step runs in background mode, where an in-memory result does
not cross the phase boundary, so a downstream step reads the rows via the
<step_name>.cache accessor (or records in the same process).
records_stored means two different things depending on which path the
task took: on the compute path it is the number of rows persisted by
this invocation (len(records) in persist_lesion_metrics); on the
read-before-compute early return, where nothing was stale, it is instead
the total row count of the step's own partition (own_accessor.count()
in lesion_calculation_task) — there being no new rows to report, the
field falls back to describing what is already 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.
Ancestors
Variables
- static
model_config
cache : CacheAccessor | None- Return theCacheAccessorfor reading this result in a DAG pipeline.
-
records : pandas.core.frame.DataFrame- Retrieve this result's rows as a DataFrame.The DataFrame is fetched from the cache on each access — it is not stored in the result object, allowing lazy access to large datasets without materialising them in memory until needed.
Returns: A
pandas.DataFramewith one row per cached record.Raises: RuntimeError: If no accessor is attached (e.g. the result was serialised across a Prefect task boundary). In a DAG pipeline use
<step_name>.cacheinstead.
Static methods
from_accessor
def from_accessor( accessor: CacheAccessor, *, records_stored: int | None = None,) ‑> Self:Inherited from:
CacheBackedResult.from_accessor :
Build a result carrying accessor, in one call rather than two.
_accessor is a PrivateAttr, so it cannot be passed to the
constructor and every producer would otherwise construct the result and
then reach in to attach the accessor. That two-step is the shape this
replaces.
Arguments
accessor: TheCacheAccessorbacking<step>.cache, scoped to the partition this result stands for.records_stored: Rows this result stands for. Defaults toaccessor.count()— the whole partition — which is what a step that persisted nothing this run reports. A step that wrote rows passes what it wrote; see the field's docstring for why the two are not interchangeable.
Returns An instance of the calling subclass, with the accessor attached.
Methods
model_post_init
def model_post_init(self: BaseModel, context: Any, /) ‑> None:Inherited from:
CacheBackedResult.model_post_init :
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.