Skip to main content

v1

Generic en-face lesion calculation step v1.

Module

Submodules

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 step ga_inference.cache).
  • filenames: List of file IDs to process.
  • cache: Cache instance to persist the lesion metrics into.
  • task_hash: Partition key for the lesion_calculation table; a config change lands in a fresh partition.
  • fovea_predictions: Optional cache accessor for fovea model inference results (background step fovea_inference.cache). None is 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 the file_id column 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 in LESION_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, or None to measure only the whole-scan aggregates. This is a RADIUS, not a diameter: a protocol naming a 3000 µm diameter region is configured as 1.5. None when no fovea landmark is available for a scan, regardless of this setting.
  • max_lesions_per_group: Cap on how many lesions a measure_lesions group 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 to aggregate_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.

Variables

  • static angular_span_min_radius_mm : float | None
  • 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.

Variables

  • static model_config
  • cache : CacheAccessor | None - Return the CacheAccessor for 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.DataFrame with 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>.cache instead.

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: The CacheAccessor backing <step>.cache, scoped to the partition this result stands for.
  • records_stored: Rows this result stands for. Defaults to accessor.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.